# AUTOGENERATED! DO NOT EDIT! File to edit: database.ipynb (unless otherwise specified).

__all__ = ['DATABASE_TABLE_NAME', 'INVENTORY_BUCKET_NAME', 'INPUT_BUCKET_NAME', 'REGION', 'ACCESS_KEY_ID',
           'SECRET_ACCESS_KEY', 'LINEKEY', 'createIndex', 'ProductDatabase', 'notify', 'keys', 'setNoUpdate',
           'setUpdate', 'fromDict', 'updateWithDict', 'loadFromS3', 'dumpToS3', 'lambdaDumpToS3', 'Product',
           'ValueUpdate', 'valueUpdate', 'lambdaUpdateProduct', 'updateS3Input', 'lambdaUpdateS3', 'lambdaSingleQuery',
           'lambdaAllQuery']

# Cell
import pandas as pd
from datetime import datetime
from pynamodb.models import Model
from pynamodb.attributes import UnicodeAttribute, NumberAttribute, JSONAttribute, BooleanAttribute, BinaryAttribute
from pynamodb.indexes import GlobalSecondaryIndex, AllProjection
from botocore.config import Config
from s3bz.s3bz import S3
from pprint import pprint
from nicHelper.wrappers import add_method, add_class_method, add_static_method
from nicHelper.dictUtil import stripDict, printDict
from nicHelper.exception import errorString
from awsSchema.apigateway import Response, Event
from dataclasses_json import dataclass_json, Undefined, CatchAll
from dataclasses import dataclass
from typing import List
from .query import Querier
from requests import post
from linesdk.linesdk import Line

import pickle, json, boto3, bz2, requests, validators, os, logging, traceback

# Cell
import os

DATABASE_TABLE_NAME = os.environ.get('DATABASE_TABLE_NAME')
INVENTORY_BUCKET_NAME = os.environ.get('INVENTORY_BUCKET_NAME')
INPUT_BUCKET_NAME = os.environ.get('INPUT_BUCKET_NAME')
REGION = os.environ.get('REGION') or 'ap-southeast-1'
ACCESS_KEY_ID = os.environ.get('USER') or None
SECRET_ACCESS_KEY = os.environ.get('PW') or None
LINEKEY= os.environ.get('LINEKEY')

try:
  DAX_ENDPOINT = os.environ['DAX_ENDPOINT']
except KeyError as e:
  DAX_ENDPOINT = None
  print(f'dax endpoint missing {e}')

print(DAX_ENDPOINT)

# Cell
def createIndex(name, rangeKeyName= None, HashKeyType = UnicodeAttribute, RangeKeyType = UnicodeAttribute):
  class ReturnSecondaryIndex(GlobalSecondaryIndex):
    class Meta:
      index_name = name
      projection = AllProjection()
      dax_read_endpoints = [DAX_ENDPOINT] if DAX_ENDPOINT else None
      dax_write_endpoints = [DAX_ENDPOINT] if DAX_ENDPOINT else None
      read_capacity_units = 1
      write_capacity_units = 1
  setattr(ReturnSecondaryIndex, name, HashKeyType(hash_key = True))
  if rangeKeyName:
    setattr(ReturnSecondaryIndex, rangeKeyName, RangeKeyType(range_key = True))
  return ReturnSecondaryIndex()

# Cell
# dont forget to import dependent classes from the relevant notebooks
class ProductDatabase(Model, Querier):
  class Meta:
    aws_access_key_id = ACCESS_KEY_ID
    aws_secret_access_key = SECRET_ACCESS_KEY
    table_name = DATABASE_TABLE_NAME
    region = REGION
    billing_mode='PAY_PER_REQUEST'
    dax_read_endpoints = [DAX_ENDPOINT] if DAX_ENDPOINT else None
    dax_write_endpoints = [DAX_ENDPOINT] if DAX_ENDPOINT else None

  iprcode = UnicodeAttribute(hash_key=True, default = '')
  cprcode = UnicodeAttribute(default = 'none', range_key = True)
  oprcode = UnicodeAttribute(default = 'none')
  pr_dpcode = UnicodeAttribute(default = 'none')
  pr_barcode = UnicodeAttribute(default = 'none')
  pr_barcode2 = UnicodeAttribute(default = 'none')
  pr_sucode1 = UnicodeAttribute(default = 'none')
  pr_suref3 = UnicodeAttribute(default= 'none')
  pr_sa_method = UnicodeAttribute(default= 'none')
  sellingPrice = NumberAttribute(default = 0)
  lastUpdate = NumberAttribute( default = 0)
  needsUpdate = UnicodeAttribute(default = 'Y')
  data = JSONAttribute()

  # indexes
  needsUpdateIndex = createIndex('needsUpdate','sellingPrice')
  cprcodeIndex = createIndex('cprcode', 'sellingPrice')
  oprcodeIndex = createIndex('oprcode', 'sellingPrice')
  pr_dpcodeIndex = createIndex('pr_dpcode', 'sellingPrice')
  pr_barcodeIndex = createIndex('pr_barcode', 'sellingPrice')
  pr_barcode2Index = createIndex('pr_barcode2', 'sellingPrice')
  pr_suref3Index = createIndex('pr_suref3', 'sellingPrice')
  pr_sa_methodIndex = createIndex('pr_sa_method', 'sellingPrice')


  TRUE = 'Y'
  FALSE = 'N'



  def __repr__(self):
    return self.returnKW(self.data)


  @staticmethod
  def returnKW(inputDict):
    outputStr = 'ProductDatabase Object\n'
    for k,v in inputDict.items():
      outputStr += f'{k} {v}\n'
    return outputStr



# Cell
@add_static_method(ProductDatabase)
def notify(message):
  line = Line(LINEKEY)
  r = line.sendNotify(message)
  if r.status_code != 200:
    print(r.json())
    return False
  return True

# Cell
@add_class_method(ProductDatabase)
def keys(cls):
  keys = list(vars(ProductDatabase)['_attributes'].keys())
  return keys

# Cell
@add_method(ProductDatabase)
def setNoUpdate(self, batch = None):
  self.needsUpdate = self.FALSE
  if batch:
    return batch.save(self)
  else:
    return self.save()

@add_method(ProductDatabase)
def setUpdate(self):
  self.needsUpdate = self.TRUE
  return self.save()

# Cell
# From dict function
@add_class_method(ProductDatabase)
def fromDict(cls, dictInput):
  logging.debug(dictInput)
  dictInput = stripDict(dictInput)
  #### extract the keys
  filteredInput = {k:v for k,v in dictInput.items() if k in cls.keys()}
  #### save whole object to dictInput
  filteredInput['data'] = dictInput
  logging.debug(filteredInput)
  return cls(**filteredInput)

# Cell
@add_class_method(ProductDatabase)
def updateWithDict(cls, originalObject:ProductDatabase, inputDict:dict ):
  data = originalObject.data
  data.update(inputDict)
  return cls.fromDict(data)

# Cell
@add_class_method(ProductDatabase)
def loadFromS3(cls, bucketName= INVENTORY_BUCKET_NAME, key = 'allData', **kwargs):
  logging.info(f'loading from {bucketName}')
  logging.info(f'user is {kwargs.get("user")}')
  return S3.load(key=key, bucket = bucketName,  **kwargs)

# Cell
@add_class_method(ProductDatabase)
def dumpToS3(cls, bucketName= INVENTORY_BUCKET_NAME, key = 'allData', **kwargs):
  ''' upload changes to s3'''
  ERRORITEMS = []
  ###### get all data
  allData = cls.loadFromS3(bucketName = bucketName, key = key, **kwargs)
  originalData = allData.copy()
  logging.debug(f'all data is {len(allData)}')

  ##### get change list
  changeList = list(cls.needsUpdateIndex.query(cls.TRUE))
  logging.debug(f'{len(changeList)} changes to update')

  ##### batch write
  with cls.batch_write() as batch:
    for dbObject in changeList:
      item = dbObject.data
      # if product doesnt exist, create an empty dict
      if not allData.get(item['iprcode']): allData[item['iprcode']] = {}
      # if cprcode doesnt exist, create an empty dict
      if not allData.get(item['iprcode']).get(item['cprcode']): allData[item['iprcode']][item['cprcode']] = {}
      # update product
      allData[item['iprcode']][item['cprcode']].update(item)
      # set no change to all data after update
      try:
        dbObject.setNoUpdate(batch=batch)
      except:
        ERRORITEMS.append(dbObject)

  ####### update s3
  if allData != originalData:
    logging.debug(f'updating')
    logging.debug(S3.save(key = 'allData',
                objectToSave = allData,
                bucket = bucketName, **kwargs)
    )
  else:
    logging.debug('no changes to update')
  logging.info(f'alldata is {next(iter(allData.items()))}')
  if ERRORITEMS:
    raise Exception(ERRORITEMS)
  return f"saved {len(list(allData.keys()))} products"

# Cell
def lambdaDumpToS3(event, _):
  try:
    result = ProductDatabase.dumpToS3()
    ProductDatabase.notify(f'successfully executed dumpToS3 {result}')
  except:
    logging.exception('error dump to s3')
    ProductDatabase.notify(f'error{errorString()}')
    return Response.returnError(errorString())

  return Response.getReturn(body = {'result': result})

# Cell
@dataclass_json(undefined=Undefined.INCLUDE)
@dataclass
class Product:
  iprcode: str
  cprcode: str
  data: CatchAll
@dataclass_json
@dataclass
class ValueUpdate:
  items: List[Product]

# Cell
@add_class_method(ProductDatabase)
def valueUpdate(cls, inputs):
    '''
      check for difference and batch update the changes in product data
    '''
    ### validate input
    try:
      validInputs = ValueUpdate.from_dict(inputs).to_dict().get('items')
    except Exception as e:
      raise KeyError(f'input failed validation {e}')
      return

    itemsUpdated = {'success':0, 'failure': 0, 'skipped': 0 ,'failureMessage':[], 'timetaken(ms)': 0}
    t0 = datetime.now()

    logging.info(f'there are {len(validInputs)} products to update')

    with cls.batch_write() as batch:
      # loop through each product
      for input_ in validInputs:
        iprcode = input_['iprcode']
        cprcode = input_['cprcode']

        # check if product is in the database, if not, create an empty class with the product code
        incumbentBr = next(cls.query(iprcode , cls.cprcode == cprcode), cls(iprcode = iprcode, cprcode = cprcode, data = {}))
        # save original data to a variable
        originalData = incumbentBr.data.copy()
        # update data
        updatedData = cls.updateWithDict(incumbentBr, input_)

        logging.info(f'incumbentBr is {incumbentBr.iprcode}\n, prcode is {iprcode}')

        # check for difference
        try:
          if updatedData.data != originalData:
            logging.info(f'product {iprcode} has changed from \n{originalData} \n{updatedData.data}')
            batch.save(updatedData)
            itemsUpdated['success'] += 1
          else:
            logging.info(f'no change for {iprcode}')
            itemsUpdated['skipped'] += 1
        except Exception as e:
          itemsUpdated['failure'] += 1
          itemsUpdated['failureMessage'].append(e)

        # log time taken
        itemsUpdated['timetaken(ms)'] = (datetime.now()- t0).total_seconds()*1000
    return itemsUpdated

# Cell
def lambdaUpdateProduct (event, _):
  products = Event.parseBody(event)['products']
  result = ProductDatabase.valueUpdate({'items':products})
  return Response.getReturn(body = result)

# Cell
@add_class_method(ProductDatabase)
def updateS3Input(cls, inputBucketName = INPUT_BUCKET_NAME, key = '', **kwargs):
  products = S3.load(key=key, bucket = inputBucketName,  **kwargs)
  updateResult = cls.valueUpdate({'items':products})
  return updateResult


# Cell
def lambdaUpdateS3(event, _):
  inputKeyName = Event.from_dict(event).key()
  try:
    updateResult = ProductDatabase.updateS3Input(
      inputBucketName=INPUT_BUCKET_NAME, key= inputKeyName)
  except:
    notify(f'error updating with s3 {errorString()}')
    return Response.returnError(errorString())



  notify(f'success update {updateResult}')
  return Response.getReturn(body = updateResult)

# Cell
def lambdaSingleQuery(event, _):
  key, value = Event.from_dict(event).firstKey()
  try:
    result = ProductDatabase.singleProductQuery({key:value}).data
  except Exception as e:
    return Response.returnError(f'{e}')
  return Response.returnSuccess(body = result)

# Cell
def lambdaAllQuery(event, _):
  url = ProductDatabase.allQuery(bucket = INVENTORY_BUCKET_NAME)
  return Response.getReturn(body = {'url': url})