import numpy as np
import scipy.optimize as so
from sympy import lambdify

def minimize(objective_func, opt_vars_list, constraints_dict, x0_initial_guess=None):
    """
    Minimizes a symbolic objective function subject to fixed-value constraints using SciPy.

    Parameters:
        objective_func   : SymPy expression defining the objective function to minimize
        opt_vars_list    : List of all symbolic variables involved in the optimization
        constraints_dict : Dictionary of fixed constraints {symbol: value}
        x0_initial_guess : Initial guess array for the optimizer (default=None → zeros with constraints applied)

    Returns:
        scipy.optimize.OptimizeResult
            The optimization result object returned by SciPy.
    """
    # Wrappers
    def _create_objective_wrapper():
        # Lambdify the symbolic expression for numerical evaluation
        objective_func_raw = lambdify(opt_vars_list, objective_func, 'numpy')
        
        def wrapper(x_flat):
            return objective_func_raw(*x_flat)
        return wrapper

    # Constraints
    def _create_constraints():
        scipy_constraints = []
        constrained_vars = list(constraints_dict.keys())

        for i, var in enumerate(opt_vars_list):
            if var in constrained_vars:
                # Capture loop variables (index, val) in closure
                fixed_value = constraints_dict[var]
                
                def constraint_fun(x, index=i, val=fixed_value):
                    return x[index] - val

                scipy_constraints.append({
                    'type': 'eq',
                    'fun': constraint_fun
                })
        return scipy_constraints

    # Initial Guess
    def _prepare_initial_guess():
        num_vars = len(opt_vars_list)
        x0 = np.zeros(num_vars) if x0_initial_guess is None else x0_initial_guess.copy()
        
        # Enforce constraints on the initial guess
        for var, val in constraints_dict.items():
            try:
                index = opt_vars_list.index(var)
                x0[index] = val
            except ValueError:
                pass # Variable not in optimization list
        return x0

    # Main Execution
    objective_wrapper = _create_objective_wrapper()
    cons = _create_constraints()
    x0 = _prepare_initial_guess()

    sol = so.minimize(objective_wrapper, x0, constraints=cons, method='SLSQP')
    
    return sol