class ChargingProblem:
    """
    Charging scheduling problem instance.

    This class stores **all data required to formulate a charging scheduling
    optimization problem** for electric vehicles (EVs) and stationary batteries.
    It contains no solver logic—its purpose is to act as a structured container
    for model parameters.

    Parameters
    ----------
    num_evs : int
        Number of electric vehicles.
    min_e_grid : float
        Minimum grid energy (used as an objective lower bound or similar).
    horizon : int
        Number of discrete time steps in the scheduling horizon.
    interval_length : float
        Length of a regular time interval.
    first_interval_length : float
        Length of the first (possibly shorter) interval.
    min_socs, max_socs : array-like
        Minimum and maximum SOC bounds for each EV.
    vip_status : array-like of bool
        Boolean VIP flag for each EV.
    ev_end_intervals : array-like
        Last interval index in which each EV is available.
    init_socs : array-like
        Initial SOC for each EV.
    target_socs : array-like
        Desired target SOC for each EV.
    ev_min_final_socs : array-like
        Minimum required final SOC for each EV.
    ev_capacities : array-like
        Battery capacities (kWh) of each EV.
    fraction_last_interval : array-like or float
        Fractional duration of the last interval for SOC integration.
    load_limits : array-like
        Maximum allowable grid load per time step.
    ev_p_max, ev_p_min : array-like
        Maximal and minimal charging powers for EVs.
    num_bat : int
        Number of stationary batteries.
    bat_capacities, bat_socs, bat_target_socs : array-like
        Capacities, initial SOCs, and target SOCs of stationary batteries.
    bat_p_max, bat_p_min : array-like
        Maximal and minimal charging powers of stationary batteries.
    p_pv : array-like
        PV production available at each time step.
    objective_hierarchy : sequence
        Sequence describing hierarchical optimization objectives.
    hierachical : bool
        Whether to use hierarchical optimization.
    soft_limits : bool or structure
        Whether certain limits should be treated as soft constraints.

    Attributes
    ----------
    num_evs : int
    min_e_grid : float
    horizon : int
    interval_length : float
    first_interval_length : float
    min_socs : array-like
    max_socs : array-like
    vip_status : array-like of bool
    ev_end_intervals : array-like
    init_socs : array-like
    target_socs : array-like
    ev_min_final_socs : array-like
    ev_capacities : array-like
    fraction_last_interval : array-like or float
    load_limits : array-like
    ev_p_max : array-like
    ev_p_min : array-like
    num_bat : int
    bat_capacities : array-like
    bat_socs : array-like
    bat_target_socs : array-like
    bat_p_max : array-like
    bat_p_min : array-like
    p_pv : array-like
    objective_hierarchy : sequence
    hierachical : bool
    soft_limits : bool or structure

    Notes
    -----
    This class **does not build or solve** any optimization model. Solver
    modules should consume this instance to construct the scheduling problem.
    """

    def __init__(self,
                 num_evs,
                 min_e_grid,
                 horizon,
                 interval_length,
                 first_interval_length,
                 min_socs,
                 max_socs,
                 vip_status,
                 ev_end_intervals,
                 init_socs,
                 target_socs,
                 ev_min_final_socs,
                 ev_capacities,
                 fraction_last_interval,
                 load_limits,
                 ev_p_max,
                 ev_p_min,
                 num_bat,
                 bat_capacities,
                 bat_socs,
                 bat_target_socs,
                 bat_p_max,
                 bat_p_min,
                 p_pv,
                 objective_hierarchy,
                 hierachical,
                 soft_limits):
        self.num_evs = num_evs
        self.min_e_grid = min_e_grid
        self.horizon = horizon
        self.interval_length = interval_length
        self.first_interval_length = first_interval_length
        self.min_socs = min_socs
        self.max_socs = max_socs
        self.vip_status = vip_status
        self.ev_end_intervals = ev_end_intervals
        self.init_socs = init_socs
        self.target_socs = target_socs
        self.ev_min_final_socs = ev_min_final_socs
        self.ev_capacities = ev_capacities
        self.fraction_last_interval = fraction_last_interval
        self.load_limits = load_limits
        self.ev_p_max = ev_p_max
        self.ev_p_min = ev_p_min
        self.num_bat = num_bat
        self.bat_capacities = bat_capacities
        self.bat_socs = bat_socs
        self.bat_target_socs = bat_target_socs
        self.bat_p_max = bat_p_max
        self.bat_p_min = bat_p_min
        self.p_pv = p_pv
        self.objective_hierarchy = objective_hierarchy
        self.hierachical = hierachical
        self.soft_limits = soft_limits

import json
import numpy as np


def _to_serializable(obj):
    """Convert common scientific types to JSON-serializable ones."""
    if isinstance(obj, np.ndarray):
        return obj.tolist()
    if isinstance(obj, (list, tuple)):
        return [_to_serializable(x) for x in obj]
    if isinstance(obj, dict):
        return {k: _to_serializable(v) for k, v in obj.items()}
    if isinstance(obj, (np.integer, np.floating)):
        return obj.item()
    return obj

def write_charging_problem(problem: ChargingProblem, filename: str):
    """
    Write a ChargingProblem instance to a JSON file.

    Parameters
    ----------
    problem : ChargingProblem
        The instance to serialize.
    filename : str
        Path to the output JSON file.
    """
    data = {
        "num_evs": problem.num_evs,
        "min_e_grid": problem.min_e_grid,
        "horizon": problem.horizon,
        "interval_length": problem.interval_length,
        "first_interval_length": problem.first_interval_length,
        "min_socs": problem.min_socs,
        "max_socs": problem.max_socs,
        "vip_status": problem.vip_status,
        "ev_end_intervals": problem.ev_end_intervals,
        "init_socs": problem.init_socs,
        "target_socs": problem.target_socs,
        "ev_min_final_socs": problem.ev_min_final_socs,
        "ev_capacities": problem.ev_capacities,
        "fraction_last_interval": problem.fraction_last_interval,
        "load_limits": problem.load_limits,
        "ev_p_max": problem.ev_p_max,
        "ev_p_min": problem.ev_p_min,
        "num_bat": problem.num_bat,
        "bat_capacities": problem.bat_capacities,
        "bat_socs": problem.bat_socs,
        "bat_target_socs": problem.bat_target_socs,
        "bat_p_max": problem.bat_p_max,
        "bat_p_min": problem.bat_p_min,
        "p_pv": problem.p_pv,
        "objective_hierarchy": problem.objective_hierarchy,
        "hierachical": problem.hierachical,
        "soft_limits": problem.soft_limits,
    }

    data = _to_serializable(data)

    with open(filename, "w") as f:
        json.dump(data, f, indent=2)

def read_charging_problem(filename: str) -> ChargingProblem:
    """
    Read a ChargingProblem instance from a JSON file
    and restore original data types exactly as produced
    by generate_problem.
    """
    with open(filename, "r") as f:
        data = json.load(f)

    # --- scalars ---
    data["num_evs"] = int(data["num_evs"])
    data["num_bat"] = int(data["num_bat"])
    data["horizon"] = int(data["horizon"])

    data["min_e_grid"] = float(data["min_e_grid"])
    data["interval_length"] = float(data["interval_length"])
    data["first_interval_length"] = float(data["first_interval_length"])

    data["hierachical"] = bool(data["hierachical"])

    # --- fraction_last_interval ---
    # may be scalar or list — preserve structure
    if isinstance(data["fraction_last_interval"], list):
        data["fraction_last_interval"] = list(data["fraction_last_interval"])
    else:
        data["fraction_last_interval"] = float(data["fraction_last_interval"])

    # --- lists that should remain lists ---
    list_fields = [
        "min_socs", "max_socs", "vip_status", "ev_end_intervals",
        "init_socs", "target_socs", "ev_min_final_socs",
        "ev_capacities", "ev_p_max", "ev_p_min",
        "load_limits", "p_pv",
        "bat_capacities", "bat_socs", "bat_target_socs",
        "bat_p_max", "bat_p_min"
    ]

    for key in list_fields:
        if key in data:
            data[key] = list(data[key])

    # --- objective hierarchy ---
    data["objective_hierarchy"] = list(data["objective_hierarchy"])

    return ChargingProblem(**data)
