import copy
import itertools

import gurobipy as gp
from time import perf_counter

from gurobipy import GRB
from src.algos.high_point_relaxation import solve_up_HPR
from src.busopt.config import ModelConfig
from src.entity.solution_entity import UpSolution

# noinspection PyShadowingNames
class Outer_relaxed_mp:

    def __init__(self, hypergraph, line_list, demand_list, config: ModelConfig):
        self.config = config
        model = gp.Model("Outer_relaxed_mp_model")
        model.setParam("OutputFlag", 0)

        """
        Create Gurobi model.
        """

        # Variables.
        l_indices = [l.line_id for l in line_list]
        x_l = model.addVars(l_indices, vtype=GRB.BINARY, name='x_l')
        z_op = model.addVar(vtype=GRB.CONTINUOUS, name='z_op', lb=0)  # Benders auxiliary variable


        # Objectives.
        model.setObjective(z_op, GRB.MINIMIZE)
        model.update()

        # register class variable
        self.model = model
        self.hypergraph = hypergraph
        self.line_list = line_list
        self.demand_list = demand_list
        self.x_l = x_l
        self.z_op = z_op
        self.output_info = {}



    def _compute_subprob_obj(self, cur_mid_sol, cur_low_sol):
        """
        Calculates the subproblem's objective function value. Q_op(x)
        """
        user_cost = self.config.vot * (sum([value[0] * value[1] for value in cur_low_sol.v_sol.values()]) + sum(cur_low_sol.t_sol.values()))
        oper_cost = sum([2 * self.config.c_bar * l.length * cur_mid_sol.f_sol[l.line_id] for l in self.line_list])
        subsidies = 0
        for l in self.line_list:
            total_flow_mile = sum([tl.flow * tl.length for r in l.routes.values() for tl in r.transit_links])
            private_portion = (l.freq - l.org_freq) / l.freq
            subsidies += total_flow_mile * self.config.s_bar * private_portion

        return {"Q_op": user_cost + oper_cost + subsidies * self.config.mu,
                "user_cost": user_cost,
                "oper_cost": oper_cost,
                "subsidies": subsidies}



    def solve(self):
        UB = float("inf")  # upper bound
        LB = -float("inf")  # lower bound
        iter_step = 0  # iteration step counter
        best_up_sol, best_mid_sol, best_low_sol = None, None, None
        best_user_cost, best_mid_oper, best_funding = None, None, None
        best_sol_iter, best_sol_time = None, None
        best_mid_revenue = None
        solution_set = []
        count_up_mid_comb = 0
        count_relax_active = 0
        self.output_info['iteration_details'] = []
        L_op = 0

        start_time = perf_counter()
        while True:
            iter_step += 1
            print(f"========================== O-BLP Master Problem: Iteration {iter_step} =================================")
            print(f"Start optimizing")
            self.model.optimize()

            if self.model.status == GRB.Status.INFEASIBLE or self.model.status == GRB.Status.UNBOUNDED:
                print(f"O-BLP Master Problem: Iteration {iter_step}, Status: {self.model.status}")
                break

            # update lower bound and solution
            LB = max(LB, self.model.objVal)
            this_up_sol = UpSolution(x_dict={key: value.X for key, value in self.x_l.items()})
            print(f"\nSolving master problem...")
            print(f"Up solution in this iteration: {this_up_sol} | Master problem Obj.value: {self.model.objVal:.2f}")

            # Termination Criteria: reach a solution once acquired
            if this_up_sol in solution_set:
                print(f"reach a solution once acquired")
                print(f"O-BLP Master Problem:  Iteration {iter_step}, Converged!")
                break
            else:
                solution_set.append(this_up_sol)

            this_mid_sol, Q_op, this_user_cost, this_oper_cost, this_subsidies = None, None, None, None, None
            relaxed_prob_obj_value = None


            print(f"\nSolving subproblem...")
            dict_hpr_results = solve_up_HPR(self.hypergraph, self.line_list, self.demand_list, this_up_sol, self.config)
            is_inner_feasible = dict_hpr_results["is_inner_feasible"]
            this_mid_sol = dict_hpr_results["best_mid_sol"]
            this_low_sol = dict_hpr_results["best_low_sol"]
            this_mid_revenue = dict_hpr_results["best_mid_revenue"]
            this_user_cost = dict_hpr_results["best_user_cost"]
            this_oper_cost = dict_hpr_results["best_oper_cost"]
            this_funding_cost = dict_hpr_results["best_funding_cost"]


            """
            add benders cut to the master problem
            if the subproblem returns an optimal solution, optimality cut is added:
                z_ip >= (gamma(x) - lb_outer) * expr + gamma(x)
                expr = sum_{1-set}x_l - sum_{0-set}x_l - len({1-set}})
            elseif subproblem is infeasible, feasibility cut is added:
                expr + 1 <= 0
            """
            expr = 0
            length_1set = 0
            for l in self.line_list:
                if abs(self.x_l[l.line_id].X - 1.0) < 1e-5:
                    expr += self.x_l[l.line_id]
                    length_1set += 1
                elif abs(self.x_l[l.line_id].X) < 1e-5:
                    expr -= self.x_l[l.line_id]
            expr -= length_1set

            if not is_inner_feasible:
                # feasibility cut
                self.model.addConstr(expr + 1.0 <= 0)
                self.model.update()
                # Print iterative information
                print(f"\n[SUMMARY]")
                print(f"Subproblem is infeasible, add a feasibility cut")
                print(f"Best incumbent solution: {best_up_sol}, {best_mid_sol}")
                print(f"LB = {LB:.2f}, UB = {UB:.2f}, Gap = {(UB - LB) * 100 / abs(UB) if UB != 0 else 100:.2f}% ")

            else:
                this_subsidies = this_funding_cost / self.config.mu if self.config.mu != 0 else 0
                Q_op = this_user_cost + this_oper_cost + this_funding_cost
                # calculate Q_op(x) and update upper bound
                if UB > Q_op:
                    UB = Q_op
                    best_up_sol = this_up_sol
                    best_mid_sol = this_mid_sol
                    best_low_sol = this_low_sol
                    best_user_cost = this_user_cost
                    best_mid_oper = this_oper_cost
                    best_funding = this_subsidies
                    best_mid_revenue = this_mid_revenue
                    best_sol_iter = iter_step
                    best_sol_time = perf_counter() - start_time

                # optimality cut
                self.model.addConstr(self.z_op >= (Q_op - L_op) * expr + Q_op)

                # Print iterative information
                print(f"\n[SUMMARY]")
                print(f"New incumbent: {this_up_sol == best_up_sol}")
                print(f"This iteration solution: {this_up_sol}, {this_mid_sol}")
                print(f"Best incumbent solution: {best_up_sol}, {best_mid_sol}")
                print(f"objective value (total cost): {Q_op:.2f}")
                print(f"passenger cost: {this_user_cost:.2f}, operating cost: {this_oper_cost:.2f}, funding (not cost): {this_subsidies:.2f}")
                print(f"LB = {LB:.2f}, UB = {UB:.2f}, Gap = {(UB - LB) * 100 / abs(UB) if UB != 0 else 100:.2f}%")

            cum_time = perf_counter() - start_time
            print(f"Cumulative elapsed time: {cum_time:.2f} secs")
            print(f"====================================================================================================\n")

            self.output_info['iteration_details'].append({
                "iter_step": iter_step,
                "objective_value": Q_op, "passenger cost": this_user_cost, "operating cost": this_oper_cost, "funding": this_subsidies,
                "lb": LB, "ub": UB, "gap": (UB - LB) * 100 / abs(UB) if UB != 0 else 100,
                "relax_bound": relaxed_prob_obj_value,
                "cumulative_elapsed_time": cum_time,
                "new_incumbent": this_up_sol == best_up_sol if this_mid_sol is not None else False,
                "this_up_sol": this_up_sol.x_sol if this_mid_sol is not None else None,
                "this_mid_sol": this_mid_sol.f_sol if this_mid_sol is not None else None,
                "best_up_sol": best_up_sol.x_sol if best_mid_sol is not None else None,
                "best_mid_sol": best_mid_sol.f_sol if best_mid_sol is not None else None
            })

        elapsed_time = perf_counter() - start_time
        iterations =  iter_step - 1
        maximum_step = 2 ** len(self.line_list)

        # Optimization summary
        print(f"\n\n ============================ O-BLP Optimization Summary ============================")
        print(f"Time Elapsed: {elapsed_time:.2f} secs")
        print(f"Convergence Iteration: {iterations}/{maximum_step}, Efficiency: {iterations / maximum_step}")
        print(f"x-f expr Iteration: {count_up_mid_comb} / {len(self.config.freq_set) ** self.config.route_num}")
        print(f"Best solution: {best_up_sol}, {best_mid_sol}")
        print(f"Best objective value (total cost): {UB}")
        print(f"Best solution found at iteration {best_sol_iter}")
        print(f"Best solution found cost: {best_sol_time:.2f} secs")
        print(f"Relative gap: {(UB - LB) * 100 / abs(UB) if UB != 0 else 100:.2f}%")
        print(f"passenger cost: {best_user_cost:.2f}, operating cost: {best_mid_oper:.2f}, funding (not cost): {best_funding:.2f}")
        print(f"Count relaxed subproblem activation: {count_relax_active}")


        # Save results
        self.output_info.update({
            "iterations": iterations,
            "max_iteration_step": maximum_step,
            "x-f_iterations": count_up_mid_comb,
            "max_f_iterations": len(self.config.freq_set) ** self.config.route_num,
            "CPU (s)": elapsed_time,
            "upper_bound": UB,
            "lower_bound": LB,
            "Gap": (UB - LB) * 100 / abs(UB) if UB != 0 else 100,
            "relaxed_activation": count_relax_active,
            "iteration_find_best": best_sol_iter,
            "time_find_best": best_sol_time,
            "optimal_obj_user_cost": best_user_cost,
            "optimal_obj_mid_oper": best_mid_oper,
            "optimal_obj_funding": best_funding,
            "optimal_mid_revenue": best_mid_revenue,
            "optimal_up_solution": best_up_sol.x_sol,
            "optimal_mid_solution": best_mid_sol.f_sol,
            "optimal_low_solution": best_low_sol.v_sol,
            "L_op": L_op,
            "config": self.config.to_dict()
        })





