#!/usr/bin/env python3
"""
Gurobipy 12.0.2 Implementation for Construction Project Optimization
"""

import gurobipy as gp
from gurobipy import GRB

def optimize_construction_projects():
    """Optimize the allocation of projects to architects to minimize total length of bridges and mills built."""
    
    # 1. MODEL & DATA SETUP
    model = gp.Model("construction_optimization")
    
    # Data from the database
    bridge_lengths = {1: 120, 2: 250, 3: 400}
    mill_lengths = {1: 350, 2: 450, 3: 550}
    architect_ids = [1, 2, 3]
    min_projects = {1: 1, 2: 1, 3: 1}
    max_projects = {1: 3, 2: 3, 3: 3}
    
    # Validate array lengths before loops
    assert len(bridge_lengths) == 3, "Bridge lengths array length mismatch"
    assert len(mill_lengths) == 3, "Mill lengths array length mismatch"
    assert len(architect_ids) == 3, "Architect IDs array length mismatch"
    assert len(min_projects) == 3, "Min projects array length mismatch"
    assert len(max_projects) == 3, "Max projects array length mismatch"
    
    # 2. VARIABLES
    # Decision variables for bridges and mills
    x = {b: model.addVar(vtype=GRB.BINARY, name=f"x_{b}") for b in bridge_lengths}
    y = {m: model.addVar(vtype=GRB.BINARY, name=f"y_{m}") for m in mill_lengths}
    
    # Assignment variables for architects
    z = {(a, b): model.addVar(vtype=GRB.BINARY, name=f"z_{a}_{b}") 
         for a in architect_ids for b in bridge_lengths}
    w = {(a, m): model.addVar(vtype=GRB.BINARY, name=f"w_{a}_{m}") 
         for a in architect_ids for m in mill_lengths}
    
    # 3. OBJECTIVE FUNCTION
    # Minimize the total length of bridges and mills built
    model.setObjective(
        gp.quicksum(bridge_lengths[b] * x[b] for b in bridge_lengths) +
        gp.quicksum(mill_lengths[m] * y[m] for m in mill_lengths),
        GRB.MINIMIZE
    )
    
    # 4. CONSTRAINTS
    
    # Minimum Projects per Architect
    for a in architect_ids:
        model.addConstr(
            gp.quicksum(z[a, b] for b in bridge_lengths) +
            gp.quicksum(w[a, m] for m in mill_lengths) >= min_projects[a],
            name=f"min_projects_{a}"
        )
    
    # Maximum Projects per Architect
    for a in architect_ids:
        model.addConstr(
            gp.quicksum(z[a, b] for b in bridge_lengths) +
            gp.quicksum(w[a, m] for m in mill_lengths) <= max_projects[a],
            name=f"max_projects_{a}"
        )
    
    # Bridge Assignment
    for a in architect_ids:
        for b in bridge_lengths:
            model.addConstr(z[a, b] <= x[b], name=f"bridge_assignment_{a}_{b}")
    
    # Mill Assignment
    for a in architect_ids:
        for m in mill_lengths:
            model.addConstr(w[a, m] <= y[m], name=f"mill_assignment_{a}_{m}")
    
    # 5. SOLVING & RESULTS
    model.optimize()
    
    if model.status == GRB.OPTIMAL:
        print(f"Optimal value: {model.objVal}")
        for b in bridge_lengths:
            if x[b].x > 1e-6:
                print(f"Bridge {b} is built.")
        for m in mill_lengths:
            if y[m].x > 1e-6:
                print(f"Mill {m} is built.")
        for a in architect_ids:
            for b in bridge_lengths:
                if z[a, b].x > 1e-6:
                    print(f"Architect {a} is assigned to bridge {b}.")
            for m in mill_lengths:
                if w[a, m].x > 1e-6:
                    print(f"Architect {a} is assigned to mill {m}.")
    elif model.status == GRB.INFEASIBLE:
        print("Problem is infeasible")
    elif model.status == GRB.UNBOUNDED:
        print("Problem is unbounded")
    
    return model

# Execute the optimization
if __name__ == "__main__":
    optimize_construction_projects()