"""
Advanced LP Techniques using Gurddy

This example demonstrates:
1. Portfolio optimization problem
2. Multi-objective optimization
3. Constraint relaxation analysis
4. Performance comparison between different problem formulations
"""

import time
import gurddy
from typing import Dict, List, Tuple


def portfolio_optimization_example():
    """Solve a portfolio optimization problem using Gurddy."""
    print("=== Portfolio Optimization Problem ===")
    
    # Investment data: expected returns and risks
    assets = ['Stock_A', 'Stock_B', 'Stock_C', 'Bond_X', 'Bond_Y']
    expected_returns = {'Stock_A': 0.12, 'Stock_B': 0.10, 'Stock_C': 0.15, 'Bond_X': 0.05, 'Bond_Y': 0.06}
    risks = {'Stock_A': 0.20, 'Stock_B': 0.15, 'Stock_C': 0.25, 'Bond_X': 0.02, 'Bond_Y': 0.03}
    
    # Correlation matrix (simplified - assume some correlations)
    correlations = {
        ('Stock_A', 'Stock_B'): 0.6,
        ('Stock_A', 'Stock_C'): 0.4,
        ('Stock_B', 'Stock_C'): 0.5,
        # Bonds are less correlated with stocks
        ('Stock_A', 'Bond_X'): -0.1,
        ('Stock_A', 'Bond_Y'): -0.1,
        ('Stock_B', 'Bond_X'): -0.1,
        ('Stock_B', 'Bond_Y'): -0.1,
        ('Stock_C', 'Bond_X'): -0.1,
        ('Stock_C', 'Bond_Y'): -0.1,
        ('Bond_X', 'Bond_Y'): 0.8,
    }
    
    print(f"Assets: {assets}")
    print(f"Expected returns: {expected_returns}")
    print(f"Risk levels: {risks}")
    print()
    
    # Solve for maximum return portfolio
    model = gurddy.Model("MaxReturn_Portfolio", "LP")
    
    # Variables: allocation weights (must sum to 1)
    weights = {}
    for asset in assets:
        weights[asset] = model.addVar(f"w_{asset}", low_bound=0, up_bound=1, cat='Continuous')
    
    # Objective: maximize expected return
    expected_return = sum(weights[asset] * expected_returns[asset] for asset in assets)
    model.setObjective(expected_return, sense='Maximize')
    
    # Constraint 1: weights sum to 1 (fully invested)
    total_weight = sum(weights[asset] for asset in assets)
    model.addConstraint(total_weight == 1.0, name='FullyInvested')
    
    # Constraint 2: risk limit (simplified risk measure)
    total_risk = sum(weights[asset] * risks[asset] for asset in assets)
    model.addConstraint(total_risk <= 0.12, name='RiskLimit')  # Max 12% risk
    
    # Constraint 3: diversification - no single asset > 40%
    for asset in assets:
        model.addConstraint(weights[asset] <= 0.4, name=f'Diversify_{asset}')
    
    # Constraint 4: minimum bond allocation (conservative requirement)
    bond_allocation = weights['Bond_X'] + weights['Bond_Y']
    model.addConstraint(bond_allocation >= 0.2, name='MinBonds')  # At least 20% in bonds
    
    # Solve
    t0 = time.perf_counter()
    solution = model.solve()
    t1 = time.perf_counter()
    
    if solution:
        print("Optimal Portfolio Allocation:")
        total_return = 0
        total_risk = 0
        for asset in assets:
            weight = solution[f"w_{asset}"]
            asset_return = weight * expected_returns[asset]
            asset_risk = weight * risks[asset]
            total_return += asset_return
            total_risk += asset_risk
            print(f"  {asset:8}: {weight:6.1%} (return: {asset_return:5.1%}, risk: {asset_risk:5.1%})")
        
        print(f"\nPortfolio Summary:")
        print(f"  Expected Return: {total_return:5.1%}")
        print(f"  Risk Level: {total_risk:5.1%}")
        print(f"  Solve Time: {t1-t0:.4f}s")
    else:
        print("No feasible portfolio found!")
    
    return solution


def transportation_problem_example():
    """Solve a transportation problem using Gurddy."""
    print("\n=== Transportation Problem ===")
    
    # Supply and demand data
    suppliers = ['Factory_1', 'Factory_2', 'Factory_3']
    customers = ['Store_A', 'Store_B', 'Store_C', 'Store_D']
    
    supply = {'Factory_1': 100, 'Factory_2': 150, 'Factory_3': 120}
    demand = {'Store_A': 80, 'Store_B': 90, 'Store_C': 70, 'Store_D': 60}
    
    # Transportation costs (per unit)
    costs = {
        ('Factory_1', 'Store_A'): 4, ('Factory_1', 'Store_B'): 6, ('Factory_1', 'Store_C'): 8, ('Factory_1', 'Store_D'): 5,
        ('Factory_2', 'Store_A'): 3, ('Factory_2', 'Store_B'): 4, ('Factory_2', 'Store_C'): 6, ('Factory_2', 'Store_D'): 7,
        ('Factory_3', 'Store_A'): 5, ('Factory_3', 'Store_B'): 3, ('Factory_3', 'Store_C'): 4, ('Factory_3', 'Store_D'): 6,
    }
    
    print(f"Suppliers: {suppliers}")
    print(f"Supply: {supply}")
    print(f"Customers: {customers}")
    print(f"Demand: {demand}")
    print(f"Total supply: {sum(supply.values())}, Total demand: {sum(demand.values())}")
    print()
    
    # Check if problem is balanced
    total_supply = sum(supply.values())
    total_demand = sum(demand.values())
    
    if total_supply != total_demand:
        print(f"Warning: Unbalanced problem (supply: {total_supply}, demand: {total_demand})")
    
    # Build model
    model = gurddy.Model("Transportation", "LP")
    
    # Variables: shipment quantities
    shipments = {}
    for supplier in suppliers:
        for customer in customers:
            var_name = f"ship_{supplier}_to_{customer}"
            shipments[(supplier, customer)] = model.addVar(var_name, low_bound=0, cat='Continuous')
    
    # Objective: minimize total transportation cost
    total_cost = sum(shipments[(s, c)] * costs[(s, c)] for s in suppliers for c in customers)
    model.setObjective(total_cost, sense='Minimize')
    
    # Supply constraints
    for supplier in suppliers:
        supplier_shipments = sum(shipments[(supplier, customer)] for customer in customers)
        model.addConstraint(supplier_shipments <= supply[supplier], name=f'Supply_{supplier}')
    
    # Demand constraints
    for customer in customers:
        customer_shipments = sum(shipments[(supplier, customer)] for supplier in suppliers)
        model.addConstraint(customer_shipments >= demand[customer], name=f'Demand_{customer}')
    
    # Solve
    t0 = time.perf_counter()
    solution = model.solve()
    t1 = time.perf_counter()
    
    if solution:
        print("Optimal Transportation Plan:")
        total_cost_value = 0
        
        # Print shipment matrix
        print(f"{'':12}", end='')
        for customer in customers:
            print(f"{customer:>10}", end='')
        print(f"{'Supply':>10}")
        
        for supplier in suppliers:
            print(f"{supplier:12}", end='')
            supplier_total = 0
            for customer in customers:
                quantity = solution[f"ship_{supplier}_to_{customer}"]
                supplier_total += quantity
                cost = quantity * costs[(supplier, customer)]
                total_cost_value += cost
                if quantity > 0:
                    print(f"{quantity:>10.1f}", end='')
                else:
                    print(f"{'':>10}", end='')
            print(f"{supply[supplier]:>10}")
        
        # Print demand row
        print(f"{'Demand':12}", end='')
        for customer in customers:
            customer_total = sum(solution[f"ship_{supplier}_to_{customer}"] for supplier in suppliers)
            print(f"{customer_total:>10.1f}", end='')
        print()
        
        print(f"\nTotal Transportation Cost: ${total_cost_value:.2f}")
        print(f"Solve Time: {t1-t0:.4f}s")
    else:
        print("No feasible transportation plan found!")
    
    return solution


def constraint_relaxation_analysis():
    """Demonstrate constraint relaxation analysis."""
    print("\n=== Constraint Relaxation Analysis ===")
    
    # Simple production problem with tight constraints
    model = gurddy.Model("Production_Tight", "LP")
    
    # Variables
    x1 = model.addVar("Product_1", low_bound=0, cat='Continuous')
    x2 = model.addVar("Product_2", low_bound=0, cat='Continuous')
    
    # Objective: maximize profit
    profit = x1 * 30 + x2 * 40
    model.setObjective(profit, sense='Maximize')
    
    # Original tight constraints
    model.addConstraint(x1 * 2 + x2 * 3 <= 100, name='Resource_A')  # Tight constraint
    model.addConstraint(x1 * 4 + x2 * 2 <= 120, name='Resource_B')  # Less tight
    model.addConstraint(x1 <= 25, name='Demand_Limit_1')
    model.addConstraint(x2 <= 30, name='Demand_Limit_2')
    
    # Solve original problem
    print("Solving original problem...")
    t0 = time.perf_counter()
    solution_original = model.solve()
    t1 = time.perf_counter()
    
    if solution_original:
        x1_val = solution_original['Product_1']
        x2_val = solution_original['Product_2']
        profit_val = x1_val * 30 + x2_val * 40
        
        print(f"Original Solution:")
        print(f"  Product_1: {x1_val:.2f}")
        print(f"  Product_2: {x2_val:.2f}")
        print(f"  Profit: ${profit_val:.2f}")
        print(f"  Solve Time: {t1-t0:.4f}s")
        
        # Check constraint utilization
        resource_a_usage = x1_val * 2 + x2_val * 3
        resource_b_usage = x1_val * 4 + x2_val * 2
        
        print(f"\nConstraint Analysis:")
        print(f"  Resource A: {resource_a_usage:.2f}/100 ({resource_a_usage/100*100:.1f}% utilized)")
        print(f"  Resource B: {resource_b_usage:.2f}/120 ({resource_b_usage/120*100:.1f}% utilized)")
        print(f"  Demand Limit 1: {x1_val:.2f}/25 ({x1_val/25*100:.1f}% utilized)")
        print(f"  Demand Limit 2: {x2_val:.2f}/30 ({x2_val/30*100:.1f}% utilized)")
    
    # Now relax the tightest constraint (Resource A) by 20%
    print(f"\nRelaxing Resource A constraint by 20%...")
    model_relaxed = gurddy.Model("Production_Relaxed", "LP")
    
    x1_r = model_relaxed.addVar("Product_1", low_bound=0, cat='Continuous')
    x2_r = model_relaxed.addVar("Product_2", low_bound=0, cat='Continuous')
    
    profit_r = x1_r * 30 + x2_r * 40
    model_relaxed.setObjective(profit_r, sense='Maximize')
    
    # Relaxed constraints
    model_relaxed.addConstraint(x1_r * 2 + x2_r * 3 <= 120, name='Resource_A_Relaxed')  # 100 -> 120
    model_relaxed.addConstraint(x1_r * 4 + x2_r * 2 <= 120, name='Resource_B')
    model_relaxed.addConstraint(x1_r <= 25, name='Demand_Limit_1')
    model_relaxed.addConstraint(x2_r <= 30, name='Demand_Limit_2')
    
    t0 = time.perf_counter()
    solution_relaxed = model_relaxed.solve()
    t1 = time.perf_counter()
    
    if solution_relaxed:
        x1_r_val = solution_relaxed['Product_1']
        x2_r_val = solution_relaxed['Product_2']
        profit_r_val = x1_r_val * 30 + x2_r_val * 40
        
        print(f"Relaxed Solution:")
        print(f"  Product_1: {x1_r_val:.2f}")
        print(f"  Product_2: {x2_r_val:.2f}")
        print(f"  Profit: ${profit_r_val:.2f}")
        print(f"  Solve Time: {t1-t0:.4f}s")
        
        if solution_original:
            profit_improvement = profit_r_val - profit_val
            print(f"\nImprovement from relaxation:")
            print(f"  Profit increase: ${profit_improvement:.2f}")
            print(f"  Percentage improvement: {profit_improvement/profit_val*100:.1f}%")
            print(f"  Shadow price of Resource A (approx): ${profit_improvement/20:.2f} per unit")


def performance_comparison():
    """Compare performance of different problem formulations."""
    print("\n=== Performance Comparison ===")
    
    # Test different problem sizes
    problem_sizes = [10, 20, 50]
    
    for n in problem_sizes:
        print(f"\nTesting {n}×{n} assignment problem...")
        
        # Create assignment problem: assign n workers to n jobs
        model = gurddy.Model(f"Assignment_{n}x{n}", "LP")
        
        # Variables: x[i][j] = 1 if worker i assigned to job j
        x = {}
        for i in range(n):
            for j in range(n):
                x[(i, j)] = model.addVar(f"x_{i}_{j}", low_bound=0, up_bound=1, cat='Continuous')
        
        # Objective: minimize total cost (random costs for demo)
        import random
        random.seed(42)  # For reproducible results
        costs = {(i, j): random.randint(1, 100) for i in range(n) for j in range(n)}
        
        total_cost = sum(x[(i, j)] * costs[(i, j)] for i in range(n) for j in range(n))
        model.setObjective(total_cost, sense='Minimize')
        
        # Constraints: each worker assigned to exactly one job
        for i in range(n):
            model.addConstraint(sum(x[(i, j)] for j in range(n)) == 1, name=f'Worker_{i}')
        
        # Constraints: each job assigned to exactly one worker
        for j in range(n):
            model.addConstraint(sum(x[(i, j)] for i in range(n)) == 1, name=f'Job_{j}')
        
        # Solve and time
        t0 = time.perf_counter()
        solution = model.solve()
        t1 = time.perf_counter()
        
        if solution:
            # Calculate objective value
            obj_value = sum(solution[f"x_{i}_{j}"] * costs[(i, j)] for i in range(n) for j in range(n))
            print(f"  Size: {n}×{n}, Variables: {n*n}, Constraints: {2*n}")
            print(f"  Optimal cost: {obj_value:.2f}")
            print(f"  Solve time: {t1-t0:.4f}s")
        else:
            print(f"  Failed to solve {n}×{n} problem")


def main():
    """Run all advanced LP examples."""
    print("Advanced Linear Programming Examples with Gurddy")
    print("=" * 60)
    
    # Run examples
    portfolio_optimization_example()
    transportation_problem_example()
    constraint_relaxation_analysis()
    performance_comparison()
    
    print("\n" + "=" * 60)
    print("Advanced LP Examples Summary:")
    print("1. Portfolio Optimization - Multi-constraint investment problem")
    print("2. Transportation Problem - Classic operations research problem")
    print("3. Constraint Relaxation - Sensitivity analysis techniques")
    print("4. Performance Comparison - Scalability testing")
    print("\nThese examples demonstrate Gurddy's capability for:")
    print("- Complex multi-constraint optimization")
    print("- Real-world business problems")
    print("- Sensitivity and what-if analysis")
    print("- Performance benchmarking")


if __name__ == "__main__":
    main()