import numpy as np
import pandas as pd
from scipy.spatial import distance_matrix
from scipy.sparse.csgraph import minimum_spanning_tree
from collections import defaultdict
import random

try:
    import pulp

    pulp_available = True
except ImportError:
    pulp = None
    pulp_available = False

try:
    import pyomo.environ as pyo_env

    pyomo_available = True
except (ImportError, AttributeError):
    pyomo_available = False
    print("Pyomo not available, falling back to PuLP")


def design_with_pyomo(
    turbines,
    substation,
    cable_specs=None,
    voltage=66000,
    power_factor=0.95,
    system_params=None,
    max_clusters=None,
    time_limit=300,
    evaluate_func=None,
    total_invest_func=None,
    get_max_capacity_func=None,
):
    """
    使用Pyomo求解器优化集电线路布局
    :param turbines: 风机DataFrame
    :param substation: 升压站坐标
    :param cable_specs: 电缆规格
    :param system_params: 系统参数（用于NPV计算）
    :param max_clusters: 最大簇数，默认基于功率计算
    :param time_limit: 求解时间限制（秒）
    :param evaluate_func: 评估函数
    :param total_invest_func: 总投资计算函数
    :param get_max_capacity_func: 获取最大容量函数
    :return: 连接列表和带有簇信息的turbines
    """
    if get_max_capacity_func:
        max_mw = get_max_capacity_func(cable_specs, voltage, power_factor)
    else:
        max_mw = 100.0
    total_power = turbines["power"].sum()
    if max_clusters is None:
        max_clusters = int(np.ceil(total_power / max_mw))
    n_turbines = len(turbines)

    all_coords = np.vstack([substation, turbines[["x", "y"]].values])
    dist_matrix_full = distance_matrix(all_coords, all_coords)

    # Simple fallback for now - use PuLP instead
    print("Pyomo not fully implemented, falling back to PuLP")
    return design_with_mip(
        turbines,
        substation,
        cable_specs,
        voltage,
        power_factor,
        system_params,
        max_clusters,
        time_limit,
        evaluate_func,
        total_invest_func,
        get_max_capacity_func,
    )
    y = pyo_env.Var(
        max_clusters_list,
        within=pyo_env.BinarySet,
        initialize=False,
        name="use_cluster",
    )

    prob.constraints += pyo_env.SummizationRule(
        pyo_env.Summ(x[i, k] for i in n_turbines_list for k in max_clusters_list) == 1,
        name="assign_one_turbine",
    )

    cluster_powers = [
        pyo_env.Summ([turbines.iloc[i]["power"] * x[i, k] for i in n_turbines_list])
        for k in max_clusters_list
    ]

    prob += pyo_env.SummizationRule(
        pyo_env.Summ(y[k] * max_mw for k in max_clusters_list), name="cluster_capacity"
    )

    for k in max_clusters_list:
        prob += pyo_env.SummizationRule(
            pyo_env.Summ(x[i, k] * turbines.iloc[i]["power"] for i in n_turbines_list),
            name=f"turbine_assign_{k}",
        )

    for k in max_clusters_list:
        prob += y[k] <= pyo_env.summation(x[i, k] for i in n_turbines_list)

    prob += pyo_env.Minimize(pyo_env.Summ([y[k] for k in max_clusters_list]))

    print(
        f"Pyomo Model: {len(prob.variables)} variables, {len(prob.constraints)} constraints"
    )

    solver = pyo_env.SolverFactory("cbc")
    print("Pyomo: Starting to solve...")
    result = solver.solve(prob, time_limit=time_limit)
    print(
        f"Pyomo: Solver status={result.solver.status}, Termination condition={result.solver.termination_condition}"
    )

    if result.solver.status == pyo_env.SolverStatus.ok:
        cluster_assign = [-1] * n_turbines
        active_clusters = []

        for k in range(max_clusters):
            if pyo_env.value(y[k]) > 0.5:
                active_clusters.append(k)

        for i in n_turbines_list:
            assigned = False
            for k in active_clusters:
                if pyo_env.value(x[i, k]) > 0.5:
                    cluster_assign[i] = k
                    assigned = True
                    break
            if not assigned and active_clusters:
                dists = [dist_matrix_full[0, i + 1] for k in active_clusters]
                cluster_assign[i] = active_clusters[np.argmin(dists)]

        clusters = defaultdict(list)
        for i, c in enumerate(cluster_assign):
            clusters[c].append(i)

        connections = []
        for c, members in clusters.items():
            if len(members) == 0:
                continue
            coords = turbines.iloc[members][["x", "y"]].values
            if len(members) > 1:
                dm = distance_matrix(coords, coords)
                mst = minimum_spanning_tree(dm).toarray()
                for i in range(len(members)):
                    for j in range(len(members)):
                        if mst[i, j] > 0:
                            connections.append(
                                (
                                    f"turbine_{members[i]}",
                                    f"turbine_{members[j]}",
                                    mst[i, j],
                                )
                            )
            dists = [dist_matrix_full[0, m + 1] for m in members]
            closest = members[np.argmin(dists)]
            connections.append((f"turbine_{closest}", "substation", min(dists)))

        turbines["cluster"] = cluster_assign
        print(
            f"Pyomo optimization completed successfully, {len(connections)} connections generated"
        )
        return connections, turbines


def design_with_mip(
    turbines,
    substation,
    cable_specs=None,
    voltage=66000,
    power_factor=0.95,
    system_params=None,
    max_clusters=None,
    time_limit=300,
    evaluate_func=None,
    total_invest_func=None,
    get_max_capacity_func=None,
):
    """
    使用混合整数规划(MIP)优化集电线路布局
    :param turbines: 风机DataFrame
    :param substation: 升压站坐标
    :param cable_specs: 电缆规格
    :param system_params: 系统参数（用于NPV计算）
    :param max_clusters: 最大簇数，默认基于功率计算
    :param time_limit: 求解时间限制（秒）
    :param evaluate_func: 评估函数
    :param total_invest_func: 总投资计算函数
    :param get_max_capacity_func: 获取最大容量函数
    :return: 连接列表和带有簇信息的turbines
    """
    if not pulp_available:
        print(
            "WARNING: PuLP library not available. MIP optimization skipped, falling back to MST."
        )
        from main import design_with_mst

        connections = design_with_mst(turbines, substation)
        return connections, turbines

    if get_max_capacity_func:
        max_mw = get_max_capacity_func(cable_specs, voltage, power_factor)
    else:
        max_mw = 100.0
    if max_clusters is None:
        max_clusters = int(np.ceil(turbines["power"].sum() / max_mw))
    n_turbines = len(turbines)

    print(
        f"MIP Model Setup: n_turbines={n_turbines}, max_clusters={max_clusters}, max_mw={max_mw:.2f} MW"
    )

    all_coords = np.vstack([substation, turbines[["x", "y"]].values])
    dist_matrix_full = distance_matrix(all_coords, all_coords)

    prob = pulp.LpProblem("WindFarmCollectorMIP", pulp.LpMinimize)

    def assign_var(i, k):
        return pulp.LpVariable(f"assign_{i}_{k}", cat="Binary")

    def cluster_var(k):
        return pulp.LpVariable(f"cluster_{k}", cat="Binary")

    # 目标函数：最小化总投资（近似为风机到升压站的总距离）
    prob += pulp.lpSum(
        [
            dist_matrix_full[0, i + 1] * assign_var(i, k)
            for i in range(n_turbines)
            for k in range(max_clusters)
        ]
    )

    for i in range(n_turbines):
        prob += pulp.lpSum([assign_var(i, k) for k in range(max_clusters)]) == 1

    for k in range(max_clusters):
        cluster_power = pulp.lpSum(
            [turbines.iloc[i]["power"] * assign_var(i, k) for i in range(n_turbines)]
        )
        prob += cluster_power <= max_mw * 1.2 * cluster_var(k)

    for k in range(max_clusters):
        for i in range(n_turbines):
            prob += assign_var(i, k) <= cluster_var(k)

    print(
        f"MIP Model: {len(prob.variables())} variables, {len(prob.constraints)} constraints"
    )

    print("MIP: Starting to solve...")
    solver = pulp.PULP_CBC_CMD(timeLimit=time_limit, msg=0, warmStart=False, path=None)
    try:
        status = prob.solve(solver)
        print(
            f"MIP: Solver status={pulp.LpStatus[prob.status]}, Objective value={pulp.value(prob.objective):.4f}"
        )
    except Exception as e:
        print(f"MIP: Solver execution failed: {e}, falling back to MST")
        from main import design_with_mst

        connections = design_with_mst(turbines, substation)
        return connections, turbines

    if pulp.LpStatus[prob.status] != "Optimal":
        print(
            f"MIP solver status: {pulp.LpStatus[prob.status]}, solution not found, falling back to MST"
        )
        print("Model feasibility check:")
        print(f"Total power: {turbines['power'].sum():.2f} MW")
        print(f"Max cluster capacity: {max_mw:.2f} MW")
        print(f"Number of clusters: {max_clusters}, Number of turbines: {n_turbines}")

        for k in range(max_clusters):
            cluster_power = pulp.value(
                pulp.lpSum(
                    [
                        turbines.iloc[i]["power"] * assign_var(i, k)
                        for i in range(n_turbines)
                    ]
                )
            )
            cluster_used = pulp.value(cluster_var(k))
            print(
                f"Cluster {k}: Power={cluster_power:.2f} MW (max {max_mw * 1.2:.2f}), Used={cluster_used}"
            )

        from main import design_with_mst

        connections = design_with_mst(turbines, substation)
        return connections, turbines

    cluster_assign = [-1] * n_turbines
    active_clusters = []
    for k in range(max_clusters):
        if pulp.value(cluster_var(k)) > 0.5:
            active_clusters.append(k)

    for i in range(n_turbines):
        assigned = False
        for k in active_clusters:
            if pulp.value(assign_var(i, k)) > 0.5:
                cluster_assign[i] = k
                assigned = True
                break
        if not assigned:
            dists = [dist_matrix_full[0, i + 1] for k in active_clusters]
            cluster_assign[i] = active_clusters[np.argmin(dists)]

    clusters = defaultdict(list)
    for i, c in enumerate(cluster_assign):
        clusters[c].append(i)

    connections = []
    for c, members in clusters.items():
        if len(members) == 0:
            continue
        coords = turbines.iloc[members][["x", "y"]].values
        if len(members) > 1:
            dm = distance_matrix(coords, coords)
            mst = minimum_spanning_tree(dm).toarray()
            for i in range(len(members)):
                for j in range(len(members)):
                    if mst[i, j] > 0:
                        connections.append(
                            (
                                f"turbine_{members[i]}",
                                f"turbine_{members[j]}",
                                mst[i, j],
                            )
                        )
        dists = [dist_matrix_full[0, m + 1] for m in members]
        closest = members[np.argmin(dists)]
        connections.append((f"turbine_{closest}", "substation", min(dists)))

    turbines["cluster"] = cluster_assign
    print(
        f"MIP optimization completed successfully, {len(connections)} connections generated"
    )
    return connections, turbines
