from System.Guid import NewGuid

import logging
import appconfig as config
from orangescape.model.engine.exception.Error import InstanceNotFoundError
from orangescape.model.server.AbstractSession import AbstractSession
from AzureData import PrimaryData

from orangescape.model.server.azure.storagemediatorservice import storagemediatorservice
db = storagemediatorservice()

def __is_valid_keyname__(keyName):
    from orangescape.model.engine.util.NumericUtil import is_a_number
    if keyName is None or len(str(keyName))==0 or is_a_number(str(keyName)[0]):# or (len(keyName)>1 and keyName[:2]=='__' and keyName[-2:]=='__'):
        return False
    return True

class PrimarySession(AbstractSession):
    def __init__(self, application):
        AbstractSession.__init__(self, application)
        self.readCache = dict()

    def createModel(self, metadata,keyName=None):
        if keyName:
            if not __is_valid_keyname__(keyName):
                raise Exception('Cannot create the sheet %s with the given keyName %s'%(metadata.getSheetName(), keyName))
            id = keyName
        else:
            id = '%s%s%s'%(metadata.getSheetName()[:3], self.__generate_pk__(), NewGuid().ToString().replace('-', ''))
        model = AbstractSession.createModel(self, metadata, id)
        return model

    def createDataObject(self, metadata, keyname):
        data = PrimaryData(keyname)
        data.PartitionKey = db.getPK(keyname)
        return data

    def readFromDatastore(self, sheetId, metadata):
        if not __is_valid_keyname__(sheetId):
            """Since key name will not begin with number so no need to to even disturb the big table"""
            return None
        sheet = self.cacheGet(sheetId)
        if sheet is None or sheet.getSheetMetadataId() != metadata.getSheetId():
            record = db.get(sheetId, metadata.getSheetId())
            if record:
                sheet = PrimaryData(SheetId = record['RowKey'], data = record)
            if sheet is not None and sheet.getSheetMetadataId() == metadata.getSheetId():
                self.cacheSet(sheetId, sheet)
            else:
                return None
        return sheet

    def getListFromDatastore(self, model, referenceCell):
        key = self.getQueryCacheKey(model.getSheetId(), referenceCell)
        idList = self.cacheGet(key)

        dbResults = None
        if idList is None:
            dbRecordDict = self.setQueryCache(key, model, referenceCell)
            return dbRecordDict
        cacheData = self.checkWithCache(referenceCell, idList, dbResults)
        return cacheData

    def setQueryCache(self, key, model, referenceCell):
        baseQuery = [(referenceCell.getSheetId(), '=', model.getSheetId())]
        limit = 1000
        dbRecords = db.query(referenceCell.getRowMetadata().getSheetMetadata().getSheetId(), baseQuery, limit)
        dbResults = [PrimaryData(SheetId = record['RowKey'], data = record) for record in dbRecords]

        noRes = len(dbResults)
        if noRes>0:
            idList = list()
            dbRecordDict = {}
            for result in dbResults:
                idList.append(result.getSheetId())
                dbRecordDict[result.getSheetId()] = result
            self.cacheSet(key, idList)
            return dbRecordDict
        else:
            self.cacheSet(key, list())
            return {}

    def checkWithCache(self, referenceCell, idList, dbResults = None):
        if idList is None or len(idList)==0:
            return dict()
        dictRef = self.cacheGetMulti(idList)
        if len(dictRef) > 0:
            if len(dictRef) < len(idList):
                diffIdList = [sheetId for sheetId in idList if not dictRef.__contains__(sheetId)]
                if len(diffIdList)>0:
                    dbResults = [PrimaryData(SheetId = record['RowKey'], data = record) for record in db.getByIdList(referenceCell.getRowMetadata().getSheetMetadata().getSheetId(), diffIdList)]
        elif not dbResults or len(dbResults)==0:
            dbResults = [PrimaryData(SheetId = record['RowKey'], data = record) for record in db.getByIdList(referenceCell.getRowMetadata().getSheetMetadata().getSheetId(), idList)]
        if dbResults:
            for result in dbResults:
                if result is not None:
                    dictRef[result.getSheetId()] = result
        return dictRef

    def queryFromDataStore(self, metadata, baseQuery, limit=1000, offset=0, orderBy=None, isDescending=False):
        baseQ = []
        if not limit or limit>1000:
            limit = 1000
        for key, val in baseQuery.iteritems():
            l = list(key)
            l.append(val)
            baseQ.append(tuple(l))
        logging.info('queryFromDataStore %s'%(baseQ))
        dbResults = [PrimaryData(SheetId = record['RowKey'], data = record) for record in db.query(metadata.getSheetId(), baseQ, limit)]
        return dbResults

    def getMultiFromDatastore(self, metadataIdDict):
        idList = metadataIdDict.keys()
        cacheData = self.cacheGetMulti(idList)

        metadataIdList = {}

        dbResults = []
        if len(cacheData) > 0:
            if len(cacheData) < len(idList):
                for id, metadataId in metadataIdDict.iteritems():
                    if not cacheData.has_key(id):
                        metadataIdList.setdefault(metadataId, list()).append(id)
                print 'getmulti partial loading from cache', metadataIdList
                if len(metadataIdList)>0:
                    for metadataId, idList in metadataIdList.iteritems():
                        dbResults.extend([PrimaryData(SheetId = record['RowKey'], data = record) for record in db.getByIdList(metadataId, idList)])
        elif not dbResults or len(dbResults)==0:
            for id, metadataId in metadataIdDict.iteritems():
                metadataIdList.setdefault(metadataId, list()).append(id)
            for metadataId, idList in metadataIdList.iteritems():
                dbResults.extend([PrimaryData(SheetId = record['RowKey'], data = record) for record in db.getByIdList(metadataId, idList)])

        if dbResults:
            for result in dbResults:
                if result is not None:
                    cacheData[result.getSheetId()] = result
        return cacheData

    def getQueryCacheKey(self, foreignKeyValue, referenceCell):
        return ''.join([foreignKeyValue, '.', referenceCell.getSheetId(), '.', referenceCell.getRowMetadata().getSheetMetadata().getSheetId()])

    def __generate_pk__(self):
        if self.getUser() and self.getUser().getValue('Company'):
            companyName = self.getUser().getValue('Company')
            return companyName[8:11]
        return 'ora'

    def getUser(self):
        return self.user

    def commit_datastore(self):
        "Commit the updated sheets in TableStore"

        dataDict = dict()
        updatedIdCache = dict()
        idList = list()
        for modelDict in self.updatedSheets.values():
            for model in modelDict.values():
                for refCell in model.getSheetMetadata().getManyToOneList():
                    if self.isReferenceChanged(model, refCell):
                        if model.get(refCell) is not None:
                            if __is_valid_keyname__(model.get(refCell)):
                                qryCacheKey = self.getQueryCacheKey(model.get(refCell), refCell)
                                if not idList.__contains__(qryCacheKey):
                                    idList.append(qryCacheKey)
                            else:
                                logging.error('AzureSession commit_datastore: Invalid SheetId :%s stored in the reference cell %s %s in the model %s for the instance %s'%(model.get(refCell), refCell.getCellDisplayName(), refCell.getCellName() or '', model.getSheetMetadata().getSheetName(), model.getSheetId()))
                                raise Exception('AzureSession commit_datastore: Invalid SheetId :%s stored in the reference cell %s %s in the model %s for the instance %s'%(model.get(refCell), refCell.getCellDisplayName(), refCell.getCellName() or '', model.getSheetMetadata().getSheetName(), model.getSheetId()))
                        if model.getOriginalData().get(refCell) is not None:
                            if __is_valid_keyname__(model.getOriginalData().get(refCell)):
                                qryCacheKey = self.getQueryCacheKey(model.getOriginalData().get(refCell), refCell)
                                if not idList.__contains__(qryCacheKey):
                                    idList.append(qryCacheKey)
                            else:
                                logging.error('AzureSession commit_datastore: Invalid SheetId (old value) :%s stored in the reference cell %s %s in the model %s for the instance %s'%(model.getOriginalData().get(refCell), refCell.getCellDisplayName(), refCell.getCellName() or '', model.getSheetMetadata().getSheetName(), model.getSheetId()))


        for modelDict in self.deletedSheets.values():
            for model in modelDict.values():
                for refCell in model.getSheetMetadata().getManyToOneList():
                    if model.getOriginalData().get(refCell) is not None:
                        if __is_valid_keyname__(model.getOriginalData().get(refCell)):
                            qryCacheKey = self.getQueryCacheKey(model.getOriginalData().get(refCell), refCell)
                            if not idList.__contains__(qryCacheKey):
                                idList.append(qryCacheKey)
                        else:
                            logging.error('AzureSession commit_datastore-delete: Invalid SheetId (old value) :%s stored in the reference cell %s %s in the model %s for the instance %s'%(model.getOriginalData().get(refCell), refCell.getCellDisplayName(), refCell.getCellName() or '', model.getSheetMetadata().getSheetName(), model.getSheetId()))


        updatedIdCache = self.cacheGetMulti(idList)
        for modelDict in self.updatedSheets.values():
            for id, model in modelDict.iteritems():
                dataDict[model.getSheetId()] = model.getDataObject()
                self.updateQueryCache(model, updatedIdCache)
                model.updateOriginalData()

        entityList = [record.getData() for record in dataDict.values()]
        size = len(entityList)
        if size>0:
            self.put_into_datastore(entityList)
            self.cacheSetMulti(dataDict)

        dataDict.clear()
        for modelDict in self.deletedSheets.values():
            for id, model in modelDict.iteritems():
                dataDict[model.getSheetId()] = model.getDataObject()
                referenceCells = model.getSheetMetadata().getManyToOneList()
                for refCell in referenceCells:
                    if model.getOriginalData().get(refCell) is not None:
                        key = self.getQueryCacheKey(model.getOriginalData().get(refCell), refCell)
                        self.removeIdFromQueryCache(key, model, updatedIdCache)

        if len(dataDict)>0:
            if config.env != config.SL and logging.getLogger().level == logging.DEBUG:
                logging.debug('Deleting the records from GFS %s'%(dataDict.keys()))
            entityList = [record.getData() for record in dataDict.values()]
            #db.delete(dataDict.values())
            db.batch([], entityList)
            self.cacheDeleteMulti(dataDict)

        if len(updatedIdCache)>0:
            self.cacheSetMulti(updatedIdCache)

        self.updatedSheets.clear()
        self.deletedSheets.clear()

        self.commitAttachments()

    def commitAttachments(self):
        pass

    def updateQueryCache(self, model, updatedIdCache):
        referenceCells = model.getSheetMetadata().getManyToOneList()
        for refCell in referenceCells:
            if self.isReferenceChanged(model, refCell):
                #logging.info('Reference is changed for the '+model.getSheetId()+' - '+refCell.getCellName())
                if model.get(refCell) is not None and len(model.get(refCell))>0:
                    "Need to add this id to new parent id cache"
                    if __is_valid_keyname__(model.get(refCell)):
                        newKey = self.getQueryCacheKey(model.get(refCell), refCell)
                        newParentIdList = updatedIdCache.get(newKey)
                        if newParentIdList is not None:
                            newParentIdList.append(model.getSheetId())
                    else:
                        logging.error('GFSSession updateQueryCache: Invalid SheetId :%s stored in the reference cell %s %s in the model %s for the instance %s'%(model.get(refCell), refCell.getCellDisplayName(), refCell.getCellName() or '', model.getSheetMetadata().getSheetName(), model.getSheetId()))
                        raise Exception('GFSSession updateQueryCache: Invalid SheetId :%s stored in the reference cell %s %s in the model %s for the instance %s'%(model.get(refCell), refCell.getCellDisplayName(), refCell.getCellName() or '', model.getSheetMetadata().getSheetName(), model.getSheetId()))
                if model.getOriginalData().get(refCell) is not None and len(model.getOriginalData().get(refCell))>0:
                    "Needs to remove this id from the old parent id cache "
                    if __is_valid_keyname__(model.getOriginalData().get(refCell)):
                        oldKey =  self.getQueryCacheKey(model.getOriginalData().get(refCell), refCell)
                        self.removeIdFromQueryCache(oldKey, model, updatedIdCache)
                    else:
                        logging.error('GFSSession updateQueryCache: Invalid SheetId (old value) :%s stored in the reference cell %s %s in the model %s for the instance %s'%(model.getOriginalData().get(refCell), refCell.getCellDisplayName(), refCell.getCellName() or '', model.getSheetMetadata().getSheetName(), model.getSheetId()))

    def removeIdFromQueryCache(self, key, model, updatedIdCache):
        list = updatedIdCache.get(key)
        if list is not None:
            logging.info('Removing the id '+model.getSheetId()+' from the old parent query cache '+key)
            list.remove(model.getSheetId())

    def isReferenceChanged(self, model, refCell):
        oldData = model.getOriginalData()
        if oldData.get(refCell) is None and model.get(refCell) is None:
            return False
        if oldData.get(refCell) is None and model.get(refCell) is not None:
            return True
        if model.get(refCell) is None and oldData.get(refCell) is not None:
            return True
        return model.get(refCell) != oldData.get(refCell)

    def put_into_datastore(self, entities):
        return db.batch(entities, [])

    def count(self, metadata, baseQuery):
        baseQ = []
        limit = 1000
        for key, val in baseQuery.iteritems():
            l = list(key)
            l.append(val)
            baseQ.append(tuple(l))
        return len(db.query(metadata.getSheetId(), baseQ, limit))

    def aggregate_query(self, metadata, limit, baseQuery, measures, dimensions, operations, orderBy = None, isDescending = False, offset=0):
        from orangescape.model.server.gae.GFSReportQuery import AggregateQuery
        entities = self.query(metadata, limit, baseQuery)
        return AggregateQuery(self, metadata, entities, measures, dimensions, operations).execute()

    def syncTables(self):
        existingTables = [r['TableName'] for r in db.getTables()]
        print existingTables
        metadataIds = [db.getTableName(metadataId) for metadataId in self.getApplication().getSheetMetadataList().keys()]
        print metadataIds
        newTableIds = list(set(metadataIds) - set(existingTables))
        print 'To be created tables..', newTableIds
        db.createTables(newTableIds)

    def cacheGet(self, key):
        data = self.readCache.get(key)
        print 'Reading key %s from readcache %s'%(key, data)
        return data

    def cacheSet(self, key, value):
        self.readCache[key] = value
        return value

    def cacheGetMulti(self, keyList):
        dataDict = self.getDictMulti(keyList)
        return dataDict

    def cacheSetMulti(self, mapping):
        self.setDictMulti(mapping)

    def cacheDeleteMulti(self, mapping):
        self.deleteDictMulti(mapping.keys())

    def getDictMulti(self, keyList):
        localDict = dict()
        for key in keyList:
            obj = self.readCache.get(key)
            if obj is not None:
                localDict[key]=obj
        return localDict

    def setDictMulti(self, mapping):
        for key,val in mapping.iteritems():
            self.readCache[key]=val

    def deleteDictMulti(self, keyList):
        for key in keyList:
            if self.readCache.__contains__(key):
                del self.readCache[key]
