# Local Dev Sandbox for the solver
# Useful for testing some concepts and functionality
# and offers a much faster feedback loop than the usual end-to-end process in Local Dev
#
# How to use:
# 1. run `compose exec meazure-solver bash`
# 2. run `python`
# 3. import this file in the python repl by `from services.solver_sandbox import SolverSandbox`
# 4. run any of the methds below e.g. `SolverSandbox.yas_elastic()`

import logging

from pulp import LpProblem, LpVariable, LpInteger, LpMinimize, LpMaximize, LpAffineExpression, LpConstraint, LpStatus, lpSum
from services.loft_service import LoftService
from lib.application_configs import ApplicationConfigs

class SolverSandbox:
    def loft_service(body = {}):
        if ApplicationConfigs.local_dev_env:
            body = {'Records': [{'eventVersion': '2.1', 'eventSource': 'aws:s3', 'awsRegion': 'us-east-1', 'eventTime': '2022-03-17T13:51:22.708Z', 'eventName': 'ObjectCreated:Put', 'userIdentity': {'principalId': 'AIDAJDPLRKLG7UEXAMPLE'}, 'requestParameters': {'sourceIPAddress': '127.0.0.1'}, 'responseElements': {'x-amz-request-id': '25ecd478', 'x-amz-id-2': 'eftixk72aD6Ap51TnqcoF8eFidJG9Z/2'}, 's3': {'s3SchemaVersion': '1.0', 'configurationId': 'testConfigRule', 'bucket': {'name': 'measure-local-solver-ingest', 'ownerIdentity': {'principalId': 'A3NL1KOZZKExample'}, 'arn': 'arn:aws:s3:::measure-local-solver-ingest'}, 'object': {'key': '40f23de0-8827-013a-a353-0242ac120010_solver_run.tar.gz', 'size': 491, 'eTag': '"2b423d91e80d931302192e781b6bd47c"', 'versionId': None, 'sequencer': '0055AED6DCD90281E5'}}}]}

        # CPNRE item bank with metadata and cases
        # body = {'Records': [{'eventVersion': '2.1', 'eventSource': 'aws:s3', 'awsRegion': 'us-east-1', 'eventTime': '2022-03-23T18:49:42.979Z', 'eventName': 'ObjectCreated:Put', 'userIdentity': {'principalId': 'AIDAJDPLRKLG7UEXAMPLE'}, 'requestParameters': {'sourceIPAddress': '127.0.0.1'}, 'responseElements': {'x-amz-request-id': 'c4efd257', 'x-amz-id-2': 'eftixk72aD6Ap51TnqcoF8eFidJG9Z/2'}, 's3': {'s3SchemaVersion': '1.0', 'configurationId': 'testConfigRule', 'bucket': {'name': 'measure-local-solver-ingest', 'ownerIdentity': {'principalId': 'A3NL1KOZZKExample'}, 'arn': 'arn:aws:s3:::measure-local-solver-ingest'}, 'object': {'key': 'e8f38480-8d07-013a-5ee6-0242ac120010_solver_run.tar.gz', 'size': 12716, 'eTag': '"94189c36aef04dde3babb462442c3af3"', 'versionId': None, 'sequencer': '0055AED6DCD90281E5'}}}]}

        # LOFT item bank with metadata and cases
        body = {'Records': [{'eventVersion': '2.1', 'eventSource': 'aws:s3', 'awsRegion': 'us-east-1', 'eventTime': '2022-03-22T19:36:53.568Z', 'eventName': 'ObjectCreated:Put', 'userIdentity': {'principalId': 'AIDAJDPLRKLG7UEXAMPLE'}, 'requestParameters': {'sourceIPAddress': '127.0.0.1'}, 'responseElements': {'x-amz-request-id': '61f320d0', 'x-amz-id-2': 'eftixk72aD6Ap51TnqcoF8eFidJG9Z/2'}, 's3': {'s3SchemaVersion': '1.0', 'configurationId': 'testConfigRule', 'bucket': {'name': 'measure-local-solver-ingest', 'ownerIdentity': {'principalId': 'A3NL1KOZZKExample'}, 'arn': 'arn:aws:s3:::measure-local-solver-ingest'}, 'object': {'key': '5971f500-8c45-013a-5d13-0242ac120010_solver_run.tar.gz', 'size': 619, 'eTag': '"a3cbba098e9f6a445cba6014e47ccaf9"', 'versionId': None, 'sequencer': '0055AED6DCD90281E5'}}}]}

        # Latest CPNRE Item Bank with metadata and cases
        body = {'Records': [{'eventVersion': '2.1', 'eventSource': 'aws:s3', 'awsRegion': 'us-east-1', 'eventTime': '2022-03-24T15:47:54.652Z', 'eventName': 'ObjectCreated:Put', 'userIdentity': {'principalId': 'AIDAJDPLRKLG7UEXAMPLE'}, 'requestParameters': {'sourceIPAddress': '127.0.0.1'}, 'responseElements': {'x-amz-request-id': '1969b1ed', 'x-amz-id-2': 'eftixk72aD6Ap51TnqcoF8eFidJG9Z/2'}, 's3': {'s3SchemaVersion': '1.0', 'configurationId': 'testConfigRule', 'bucket': {'name': 'measure-local-solver-ingest', 'ownerIdentity': {'principalId': 'A3NL1KOZZKExample'}, 'arn': 'arn:aws:s3:::measure-local-solver-ingest'}, 'object': {'key': 'ab40ca20-8db7-013a-a88f-0242ac120013_solver_run.tar.gz', 'size': 24111, 'eTag': '"718a1a17b5dd5219b8e179bfd1ddf1ca"', 'versionId': None, 'sequencer': '0055AED6DCD90281E5'}}}]}

        # Latest LOFT Item Bank with metadata and cases with target variance
        body = {'Records': [{'eventVersion': '2.1', 'eventSource': 'aws:s3', 'awsRegion': 'us-east-1', 'eventTime': '2022-03-25T18:03:18.829Z', 'eventName': 'ObjectCreated:Put', 'userIdentity': {'principalId': 'AIDAJDPLRKLG7UEXAMPLE'}, 'requestParameters': {'sourceIPAddress': '127.0.0.1'}, 'responseElements': {'x-amz-request-id': '204c718f', 'x-amz-id-2': 'eftixk72aD6Ap51TnqcoF8eFidJG9Z/2'}, 's3': {'s3SchemaVersion': '1.0', 'configurationId': 'testConfigRule', 'bucket': {'name': 'measure-local-solver-ingest', 'ownerIdentity': {'principalId': 'A3NL1KOZZKExample'}, 'arn': 'arn:aws:s3:::measure-local-solver-ingest'}, 'object': {'key': 'beb35dc0-8e93-013a-5807-0242ac120013_solver_run.tar.gz', 'size': 24112, 'eTag': '"a5a4aad0eb8c9d9af2aad9684437022a"', 'versionId': None, 'sequencer': '0055AED6DCD90281E5'}}}]}

        LoftService(body).process()

    def yosh_loop():
        Items = [1,2,3,4,5]
        tif = {
          1: 0.2,
          2: 0.5,
          3: 0.3,
          4: 0.8,
          5: 0.1
        }
        iif = {
          1: 0.09,
          2: 0.2,
          3: 0.113,
          4: 0.3,
          5: 0.1
        }
        drift = 0.0
        drift_limit = 0.2
        iif_target = 0.5
        tif_target = 0.9
        item_vars = LpVariable.dicts("Item", Items, cat="Binary")
        while drift <= drift_limit:
          prob = LpProblem("tif_tcc_test", LpMinimize)
          prob += lpSum([(tif[i] + iif[i]) * item_vars[i] for i in Items]), "TifTccSum"
          prob += lpSum([item_vars[i] for i in Items]) == 3, "TotalItems"
          prob += lpSum([tif[i] * item_vars[i] for i in Items]) >= tif_target - (tif_target * drift), 'TifMin'
          prob += lpSum([tif[i] * item_vars[i] for i in Items]) <= tif_target  + (tif_target * drift), 'TifMax'
          prob += lpSum([iif[i] * item_vars[i] for i in Items]) >= iif_target - (iif_target * drift), 'TccMin'
          prob += lpSum([iif[i] * item_vars[i] for i in Items]) <= iif_target + (iif_target * drift), 'TccMax'
          prob.solve()
          print(prob)
          if LpStatus[prob.status] == "Infeasible":
            print('attempt infeasible')
            for v in prob.variables():
              print(v.name, "=", v.varValue)

            drift += 0.02
          else:
            print(f"solution found with drift of {drift}!")
            for v in prob.variables():
              print(v.name, "=", v.varValue)
            break

    def yas_elastic(tif_targets, tcc_targets): # [50, 55, 46], [60, 40, 50]
        Items   = [1,2,3,4,5,6,7,8,9,10]

        iif = {
          1: 5,
          2: 5,
          3: 5,
          4: 10,
          5: 10,
          6: 10,
          7: 15,
          8: 20,
          9: 20,
          10: 20
        }
        # ---
        irf = {
          1: 5,
          2: 5,
          3: 5,
          4: 10,
          5: 10,
          6: 10,
          7: 15,
          8: 20,
          9: 20,
          10: 20
        }

        items = LpVariable.dicts('Item', Items, cat='Binary')
        drift     = 0
        max_drift = 25# 25% elasticity

        while drift <= max_drift:
            drift_percent = drift / 100
            problem = LpProblem('TIF_TCC', LpMinimize)

            # objective function
            problem += lpSum([items[i] for i in Items])

            # Constraint 1
            problem += lpSum([items[i] for i in Items]) == 5, 'TotalItems'

            # Our own "Elastic Constraints"
            for tif_target in tif_targets:
              print(f"Calculating TIF target of {tif_target} with drift of {drift}%")
              problem += lpSum(
                [iif[i] * items[i] for i in Items]
              ) >= tif_target - (tif_target * drift_percent)
              problem += lpSum(
                [iif[i] * items[i] for i in Items]
              ) <= tif_target + (tif_target * drift_percent)

            for tcc_target in tcc_targets:
              print(f"Calculating TIF target of {tcc_target} with drift of {drift}%")
              problem += lpSum(
                [irf[i] * items[i] for i in Items]
              ) >= tcc_target - (tcc_target * drift_percent)
              problem += lpSum(
                [irf[i] * items[i] for i in Items]
              ) <= tcc_target + (tcc_target * drift_percent)

            problem.solve()

            if LpStatus[problem.status] == 'Infeasible':
                print(f"attempt infeasible for drift of {drift}")

                for v in problem.variables(): print(v.name, "=", v.varValue)

                # if drift == max_drift: breakpoint();

                print(problem.objective.value())
                print(problem.constraints)
                print(problem.objective)

                drift += 1
            else:
                print(f"solution found with drift of {drift}!")

                for v in problem.variables(): print(v.name, "=", v.varValue);

                print(problem.objective.value())
                print(problem.constraints)
                print(problem.objective)

                break

    # Implementation of the Whiskas Cat problem, with elastic constraints
    # https://www.coin-or.org/PuLP/CaseStudies/a_blending_problem.html
    # https://stackoverflow.com/questions/27278691/how-can-an-elastic-subproblem-in-pulp-be-used-as-a-constraint?noredirect=1&lq=1
    def whiskas():
        # Creates a list of the Ingredients
        Ingredients = ['CHICKEN', 'BEEF', 'MUTTON', 'RICE', 'WHEAT', 'GEL']

        # A dictionary of the costs of each of the Ingredients is created
        costs = {'CHICKEN': 0.013,
                 'BEEF': 0.008,
                 'MUTTON': 0.010,
                 'RICE': 0.002,
                 'WHEAT': 0.005,
                 'GEL': 0.001}

        # A dictionary of the protein percent in each of the Ingredients is created
        proteinPercent = {'CHICKEN': 0.100,
                          'BEEF': 0.200,
                          'MUTTON': 0.150,
                          'RICE': 0.000,
                          'WHEAT': 0.040,
                          'GEL': 0.000}

        # A dictionary of the fat percent in each of the Ingredients is created
        fatPercent = {'CHICKEN': 0.080,
                      'BEEF': 0.100,
                      'MUTTON': 0.110,
                      'RICE': 0.010,
                      'WHEAT': 0.010,
                      'GEL': 0.000}

        # A dictionary of the fibre percent in each of the Ingredients is created
        fibrePercent = {'CHICKEN': 0.001,
                        'BEEF': 0.005,
                        'MUTTON': 0.003,
                        'RICE': 0.100,
                        'WHEAT': 0.150,
                        'GEL': 0.000}

        # A dictionary of the salt percent in each of the Ingredients is created
        saltPercent = {'CHICKEN': 0.002,
                       'BEEF': 0.005,
                       'MUTTON': 0.007,
                       'RICE': 0.002,
                       'WHEAT': 0.008,
                       'GEL': 0.000}

        logging.info('Running Test...')

        # create problem
        problem = LpProblem("The Whiskas Problem", LpMinimize)

        # A dictionary called 'ingredient_vars' is created to contain the referenced Variables
        ingredient_vars = LpVariable.dicts("Ingr", Ingredients, 0)

        # set objective
        problem += lpSum([costs[i]*ingredient_vars[i] for i in Ingredients]), "Total Cost of Ingredients per can"

        # The five constraints are added to 'prob'
        problem += lpSum([ingredient_vars[i] for i in Ingredients]) == 100, "PercentagesSum"
        problem += lpSum([proteinPercent[i] * ingredient_vars[i] for i in Ingredients]) >= 8.0, "ProteinRequirement"
        problem += lpSum([fatPercent[i] * ingredient_vars[i] for i in Ingredients]) >= 6.0, "FatRequirement"
        problem += lpSum([fibrePercent[i] * ingredient_vars[i] for i in Ingredients]) <= 2.0, "FibreRequirement"
        problem += lpSum([saltPercent[i] * ingredient_vars[i] for i in Ingredients]) <= 0.4, "SaltRequirement"

        # ELASTICIZE
        # c6_LHS_A = LpAffineExpression([ingredient_vars])
        c6_LHS = LpAffineExpression([(ingredient_vars['GEL'],1), (ingredient_vars['BEEF'],1)])
        c6= LpConstraint(e=c6_LHS, sense=-1, name='GelBeefTotal', rhs=30)
        c6_elastic = c6.makeElasticSubProblem(penalty = 100, proportionFreeBound = .10)

        problem.extend(c6_elastic)

        print(problem)

        # solve problem
        problem.solve()

        # The status of the solution is printed to the screen
        print("Status:", LpStatus[problem.status])

        # Each of the variables is printed with it's resolved optimum value
        for v in problem.variables():
            print(v.name, "=", v.varValue)

        # The optimised objective function value is printed to the screen
        print("Total Cost of Ingredients per can = ", problem.objective.value())