# -*- coding: utf-8 -*-

"""
    This code is called by the Oracle Demand Management Cloud -> OCI Data Science Rest Service
    Two methods are required by OCI Data Science: load_model(model_file_name=model_name) and predict(data, model=load_model()).
    In this implementation, we are only using the predict method.  We are using the same model deployment to service multiple models.
    We call a different model, depending on the customer site.  In this example, we have customer sites 1,2,9,18,20 and 21.
    Depending on the customer site, we will load the pickeled xgboost model corresponding to that customer site. 
    For customer site 1, we will load xgb_store_1.mdl via pickel.  

    For each model, we have a corresponding list of default values.  These are stored in default_<CUSTOMER SITE NUMBER.csv
    For customer site 1, we will load default_1.csv.
    The ML model, expects each of the attributes in in default_1.csv to be included in the call to the predict() method.  
    By including a list of default values, you can ensure that all of the expected features will be included in the call to predict().

"""

import json
import os
import pickle
import numpy as np
import pandas as pd
import xgboost as xgb

"""
    The model name below is a placeholder.  We will actually load the correct ml model file based on the customer site of the rest call.
"""


model_name = f"test_model.mdl"


"""
   Inference script. This script is used for prediction by scoring server when schema is known.
"""


def load_model(model_file_name=model_name):
    return "DEFAULT"

def predict(data, model=load_model()):
    """
    Returns prediction given the model and data to predict
    Parameters
    ----------
    model: a dummy model name
    data: This is the json payload of the rest call made from Demand Management Cloud's External ML Model.  The payload will include all of the features
          defined in the External ML Engine profile.  The rest input may look something like this:

{
	"FEATURESET": {
		"COMBINATION_0": [
			{
				"DATATYPE": "STRING",
				"CATEGORICAL": "TRUE",
				"LEVEL_VALUE": [26628095],
				"VALUE": ["383103"],
				"TYPE": "LEVEL",
				"LEVEL_ID": "1",
				"NAME": "Item"
			},
			{
				"DATATYPE": "STRING",
				"CATEGORICAL": "TRUE",
				"LEVEL_VALUE": [999990034220796],
				"VALUE": ["2"],
				"TYPE": "LEVEL",
				"LEVEL_ID": "43",
				"NAME": "Customer Site"
			},
			{
				"DATATYPE": "STRING",
				"CATEGORICAL": "TRUE",
				"LEVEL_VALUE": [1544356],
				"VALUE": ["9634"],
				"TYPE": "LEVEL",
				"LEVEL_ID": "1456055",
				"NAME": "Item Class"
			},
			{
				"DATATYPE": "STRING",
				"CATEGORICAL": "TRUE",
				"LEVEL_VALUE": [1544148],
				"VALUE": ["A"],
				"TYPE": "LEVEL",
				"LEVEL_ID": "1456056",
				"NAME": "Item Family"
			},
			{
				"DATATYPE": "NUMBER",
				"CATEGORICAL": "FALSE",
				"VALUE": [4,5,6,7,1,2,3],
				"TYPE": "MEASURE",
				"NAME": "DAY_NUM_IN_WEEK"
			}
             ]
        }
        In this case, the measure DAY_NUM_IN_WEEK is numeric.  The level values are all categorical.  The categorical values will be on-hot-encoded to is_<LEVEL_VALUE>.
        The numeric values will be kept as is.
    -------
    predictions: Output from scoring server
        Format: {'prediction':output from model.predict method}
        In our example, we get the ruturn value of
         {"prediction": [{"COMBINATION_0": [0.3129279911518097, 0.3857766389846802, 0.3429827094078064, 0.5973849296569824, 0.5948182344436646, 0.35237306356430054,            0.35656455159187317]}

         By including several combinations, we can perform a batch of preductions at one time.

    """
    model_type = check_model_type(data)
    
    if model_type == "COPY":
        return copy_model_predict(data)
    else:
        return standard_predict(data)

"""
  check_model_type(data): Check to see if this is a standard predict, or the "COPY" predict for testing out this feature.
    If a measure name in the engine profile is named with the pattern "... COPY ..." or "... CP ..." the copy model will be used.
    This model (for testing purposes) returns the value of the first measure found in the engine profile.  It essentially copies one
    measure to another.
"""
    
def check_model_type(data):
    combination_array = data["FEATURESET"]

    for cur_key in combination_array:
        #print(cur_key)
        cur_data = combination_array[cur_key]
        for cur_feature in cur_data:
            if cur_feature["TYPE"] == "MEASURE":
                feature_name = cur_feature["NAME"]
                if ' CP ' in feature_name or ' COPY ' in feature_name:
                    print("**CUR DATA**")
                    print(cur_feature["NAME"])
                    print("--CUR DATA--")
                    return "COPY"
    return "STANDARD"

"""
standard_predict(data):
  iterate over each combination in the data set.  Call the ML Model predict() for each combination.
"""
def standard_predict(data):
    return_values = []
    # Get the customer number
    customer_number = get_customer(data)
    #get array of combinations to iterate over
    combination_array = data["FEATURESET"]
    model = None
    previous_model = None
    for cur_key in combination_array:
        cur_data = combination_array[cur_key]
        #Put the current combinations into a form that the predict method understands.
        #first, pull in the default data set for the customer number.  This will be in the default_<Customer site id>.csv file.
        model_input = get_sample_data(cur_data, customer_number)
        #Next fill in the default values with the ones passed in from the ret call.
        model_input = format_data(model_input, cur_data)
            
        print(model_input)
        #Now load the appropriate model, depending on the customer site.
        model, previous_model = get_model(cur_data, model, previous_model)
        #Run the model.predict() method.
        predictions = model.predict(model_input)
        #add the prediction for this combination into the return values object.
        new_prediction = {cur_key : predictions.tolist()}
        return_values.append(new_prediction)
    #Return the predictions for all of the combinations
    return {'prediction': return_values}

def copy_model_predict(data):
    return_values = []
    combination_array = data["FEATURESET"]
    model = None
    previous_model = None
    for cur_key in combination_array:
        cur_data = combination_array[cur_key]
        copy_predictions = get_copy_predictions(cur_data)
        new_prediction = {cur_key : copy_predictions}
        return_values.append(new_prediction)

    return {'prediction': return_values}

"""
  get_model(data, model, previous_model):
  This method looks through the data of the current combination, to see which customer site it belongs to.  It will then
  load the model corresponding to that customer site.  It will default to customer site #9 if no matching customer site is found.
  The model will be loaded using pickel.load().
"""
def get_model(data, model, previous_model):
    model_file_name = "xgb_store_9.mdl"
    #model_file_type = "linreg_store_"
    model_file_type = "xgb_store_"
    for rec in data:
        if rec["TYPE"] == "LEVEL" and rec["NAME"].upper() == "CUSTOMER SITE":
            recval = rec["VALUE"]
            model_file_root = recval[0]
            if model_file_root == "1" or model_file_root == "2":
                None
            else:
                model_file_root="1"
            model_file_name = model_file_type+model_file_root+".mdl"
            print("Using Model "+model_file_name)
    
    if model_file_name==previous_model and model is not None:
        return model, model_file_name
    model_dir = os.path.dirname(os.path.realpath(__file__))
    contents = os.listdir(model_dir)
    model_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), model_file_name)
    if os.path.getsize(model_path) > 0:
        if model_file_name in contents:
            with open(model_path, "rb") as file:
                loaded_model = pickle.load(file)
    return loaded_model, model_file_name

"""
  get_sample_data(data, customer_number): This method loads the default values from the csv default_<Customer Site>.csv
  This default file should be included with the model deployment.
"""
def get_sample_data(data, customer_number):
    
    data_dir = os.path.dirname(os.path.realpath(__file__))
    contents = os.listdir(data_dir)
    data_file_name = "default_" + customer_number + ".csv"
    if data_file_name in contents:
        with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), data_file_name), "rb") as file:
            return pd.read_csv(file)
        
    return "DEFAULT"

"""
  get_customer(data): This method Finds the customer site of the current combination.  If there is no customer site, it will default to site 9
"""
def get_customer(data):
    combination_array = data["FEATURESET"]
    model_file_root = "1"
    for cur_key in combination_array:
        cur_data = combination_array[cur_key]
        for cur_feature in cur_data:
            if cur_feature["TYPE"] == "LEVEL" and cur_feature["NAME"].upper() == "CUSTOMER SITE":
                feature_name = cur_feature["NAME"]
                recval = cur_feature["VALUE"]
                model_file_root = recval[0]
                if model_file_root == "1" or model_file_root == "2":
                    #print("specific model found")
                    None
                else:
                    model_file_root="1"
    return model_file_root

"""
  format_data(model_input, data_input): This method scans the current combination and replaces the defaults with the actual feature values that comes from the rest call.
"""
def  format_data(model_input, data_input):
    data = data_input
    num_buckets = get_buckets(data)
    model_input = fill_input(model_input, num_buckets)
    
    for cur_bucket in range(0,num_buckets-1):    
        #print("BUCKET ITER = " + str(cur_bucket))
        for feature in data:
            cur_bucket_num=cur_bucket
            if len(feature["VALUE"]) == 1:
                cur_bucket_num = 0
            feature_name = feature["NAME"]
            feat = feature_name
            feature_value = feature["VALUE"][cur_bucket_num]
            # In this model, all level values are treated as categorical.  Your model may be different.
            if feature["TYPE"] == "LEVEL":
                #on hot encode the level value with an is_ prefix.  Set the value to 1.
                feature_name = "is_" + str(feature["VALUE"][cur_bucket_num]).replace( ' ', '_') 
                feature_value = 1
            # For categorival values, one hot encode the feature name, and set the value to 1
            elif feature["CATEGORICAL"] == "TRUE":
                feature_name = "is_" + str(feature["VALUE"][cur_bucket_num]).replace( ' ', '_').upper()
                feature_value = 1
            # Here is an example of special handling for measures whose names do not match the values the model was trained on.  We can use special processing to sync up the column names without having to retrain the model.
            elif feature["TYPE"] == "MEASURE":
                if feat in("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"):
                    feature_name = "is_" + feat
                    feature_value = feature["VALUE"][cur_bucket_num]  
                    if feature_value is None:
                        feature_value = 0
                    #print("MEAS...")
                elif feat=="DAY_NUM_IN_WEEK":
                    feature_value = feature["VALUE"][cur_bucket_num]
                    if feature_value is None:
                        feature_value = 0
                    else:
                        feature_value = feature_value - 1
                elif feat=="WEEK_NUM_IN_MONTH":
                    feature_value = feature["VALUE"][cur_bucket_num]
                    feature_name = get_week_value(feature_value)
                    feature_value = 1

                else:
                    feature_value = feature["VALUE"][cur_bucket_num]
                    if feature_value is None:
                        feature_value = 0
                    feature_name = feature_name.replace( ' ', '_').upper()
            # For numeric values, just set the column name to the value passed in from the feature in the engine profile.
            else:
                feature_value = feature["VALUE"][cur_bucket_num]
                feature_name = feature_name.replace( ' ', '_').upper()
            
            if feature_name in model_input.columns.values:
                model_input.at[cur_bucket, feature_name] =  convert_value(feature_value)
                
            else:
                if feat != 'Day' and feat != 'Customer Site':
                    print("CANNOT FIND FEATURE IN MODEL => ", feature_name )             
    return model_input

"""
  convert_value(value): This method ensures that value passed from the engine profile is in the format that ML model understands.  Convert YES and NO to 1 and 0
"""

def convert_value(value):
    return_value = value
    if value == 'Yes' or value == 'YES':
       return_value = 1
    elif value == 'No' or value == 'NO':
       return_value = 0
    
    return return_value

"""
  get_week_value(week_value): This custom method converts the a numeric week measure value into is_First_Week, is_Second_Week, etc..  It is aligning the DM data with the columns in the trained model
"""
def get_week_value(week_value):
    if week_value == 1:
        return 'is_First_Week';
    elif week_value == 2:
        return 'is_Second_Week';
    elif week_value == 3:
        return 'is_Third_Week';
    elif week_value == 4:
        return 'is_Fourth_Week';
    elif week_value == 5:
        return 'is_Fifth_Week';
    else:
        return None;

"""
   get_buckets(data): This method determines the number of time buckets that exist in this rest call.
"""         
def  get_buckets(data):
    buckets = 0
    
    for feature in data:
            value_list = feature["VALUE"]
            if len(value_list) > buckets:
                buckets = int(len(value_list))

    return buckets


"""
get_copy_predictions(data): Return the value of the first measure when running a copy prediction.  This
  is a model used for testing.
"""
def get_copy_predictions(data):
    return_data = []
    for feature in data:
        if feature["TYPE"] == "MEASURE":
            return_data = feature["VALUE"]
            break

    return return_data

    
    

"""
Fill the input of the data sample, repeating the rows
"""
def  fill_input(model_input, num_buckets):
    new_input = model_input.loc[model_input.index.repeat(num_buckets)].reset_index(drop=True)
    return new_input

    





