from typing import Dict, List, Tuple
from dataclasses import dataclass
import statistics
import math

# Type definitions
Person = str
Satisfaction = float
Pairing = Tuple[Person, Person]
PairingScheme = List[Pairing]
PreferenceLists = Dict[Person, List[Person]]
Preferences = Dict[Person, Dict[Person, Satisfaction]]

@dataclass
class PairingResult:
    """Class to store the result of a pairing scheme evaluation."""
    pairing_scheme: PairingScheme
    min_satisfaction: float
    avg_satisfaction: float
    individual_satisfaction: Dict[Person, float]

def compute_preferences_from_lists(preference_lists: PreferenceLists, people: List[Person]) -> Preferences:
    """
    Compute normalized preference values from preference lists.
    For a list of length n, the highest preference is 1.0 and the lowest is 1/n.
    If a person is not in the preference list, they get the lowest possible score.
    """
    preferences: Preferences = {}
    
    for person in people:
        preferences[person] = {}
        pref_list = preference_lists.get(person, [])
        
        # Process people in the preference list
        if pref_list:
            list_length = len(pref_list)
            
            for position, partner in enumerate(pref_list):
                # Calculate preference value: starts at 1.0, decreases linearly to 1/list_length
                preference_value = 1.0 - (position / list_length) * (1.0 - 1.0/list_length)
                preferences[person][partner] = preference_value
        
        # Assign lowest score to anyone not in the preference list
        lowest_score = 1.0/len(people) if pref_list else 0.0
        for partner in people:
            if partner != person and partner not in preferences[person]:
                preferences[person][partner] = lowest_score
    
    return preferences

def validate_preference_lists(preference_lists: PreferenceLists, people: List[Person]) -> None:
    """Validate preference lists to ensure they don't contain duplicates or invalid people."""
    all_people_set = set(people)
    
    for person, pref_list in preference_lists.items():
        # Check if the person is valid
        if person not in all_people_set:
            raise ValueError(f"Person '{person}' in preference lists is not in the people list")
        
        # Check for duplicates in preference list
        if len(pref_list) != len(set(pref_list)):
            raise ValueError(f"Duplicate entries in preference list for '{person}'")
        
        # Check if all entries in preference list are valid
        for preferred_person in pref_list:
            if preferred_person not in all_people_set:
                raise ValueError(f"Unknown person '{preferred_person}' in preference list for '{person}'")
            
            # Make sure person doesn't prefer themselves
            if preferred_person == person:
                raise ValueError(f"Person '{person}' lists themselves in their preference list")

def generate_all_pairings(people: List[Person]) -> List[PairingScheme]:
    """Generate all possible ways to pair the given people."""
    if len(people) % 2 != 0:
        raise ValueError("Number of people must be even")
    
    # Helper function to generate all possible pairings recursively
    def generate_pairings_helper(remaining_people: List[Person]) -> List[PairingScheme]:
        if not remaining_people:
            return [[]]
        
        person = remaining_people[0]
        new_remaining = remaining_people[1:]
        result: List[PairingScheme] = []
        
        for i in range(len(new_remaining)):
            partner = new_remaining[i]
            pairing = (person, partner) if person < partner else (partner, person)
            
            # Generate all pairings for the remaining people
            sub_pairings = generate_pairings_helper(new_remaining[:i] + new_remaining[i+1:])
            
            # Add the current pairing to each sub-pairing
            for sub_pairing in sub_pairings:
                result.append([pairing] + sub_pairing)
        
        return result
    
    result = generate_pairings_helper(people)
    
    # Assert that the number of pairings matches the mathematical formula
    assert len(result) == calculate_expected_pairing_count(len(people)), "Pairing count mismatch"
    
    return result

def evaluate_pairing_scheme(
    pairing_scheme: PairingScheme, 
    preferences: Preferences
) -> PairingResult:
    """Evaluate a pairing scheme based on satisfaction scores."""
    individual_satisfaction: Dict[Person, float] = {}
    
    # Calculate each person's satisfaction
    for pair in pairing_scheme:
        person1, person2 = pair
        
        # Check if the preferences exist, otherwise set to 0 (no preference)
        satisfaction1 = preferences.get(person1, {}).get(person2, 0.0)
        satisfaction2 = preferences.get(person2, {}).get(person1, 0.0)

        individual_satisfaction[person1] = satisfaction1
        individual_satisfaction[person2] = satisfaction2
    
    # Calculate metrics
    min_satisfaction = min(individual_satisfaction.values())
    avg_satisfaction = statistics.mean(individual_satisfaction.values())
    
    return PairingResult(
        pairing_scheme=pairing_scheme,
        min_satisfaction=min_satisfaction,
        avg_satisfaction=avg_satisfaction,
        individual_satisfaction=individual_satisfaction
    )

def find_optimal_pairing(
    people: List[Person], 
    preference_lists: PreferenceLists
) -> PairingResult:
    """
    Find the optimal pairing of people based on preference lists.
    Optimize first for the minimum satisfaction, then for the average satisfaction.
    Handles incomplete preference lists by assigning the lowest score to unlisted people.
    """

    # Validate preference lists
    validate_preference_lists(preference_lists, people)
    
    # Compute preferences from the preference lists
    preferences = compute_preferences_from_lists(preference_lists, people)
    
    all_pairing_schemes = generate_all_pairings(people)
    evaluated_schemes = [evaluate_pairing_scheme(scheme, preferences) for scheme in all_pairing_schemes]
    
    # Sort by minimum satisfaction (descending), then by average satisfaction (descending)
    evaluated_schemes.sort(
        key=lambda result: (result.min_satisfaction*0.8+result.avg_satisfaction*0.2), 
        reverse=True
    )
    
    return evaluated_schemes[0]

def print_pairing_results(result: PairingResult) -> None:
    """Print the results of the optimal pairing scheme in a readable format."""
    print("Optimal Pairing Scheme:")
    for pair in result.pairing_scheme:
        person1, person2 = pair
        print(f"  {person1} paired with {person2}")
    
    print("\nIndividual Satisfaction:")
    for person, satisfaction in sorted(result.individual_satisfaction.items()):
        print(f"  {person}: {satisfaction:.4f}")
    
    print(f"\nMinimum Satisfaction: {result.min_satisfaction:.4f}")
    print(f"Average Satisfaction: {result.avg_satisfaction:.4f}")

def calculate_expected_pairing_count(n_people: int) -> int:
    """
    Calculate the expected number of possible pairings for n people.
    Formula: (2n)! / (2^n * n!) where n is the number of pairs.
    """
    if n_people % 2 != 0:
        raise ValueError("Number of people must be even")
    
    n_pairs = n_people // 2
    return math.factorial(n_people) // (2**n_pairs * math.factorial(n_pairs))

# Example usage
def main() -> None:
    # Example preference lists (in order of preference)
    preference_lists: PreferenceLists = {
        "Alice": ["Julia", "Bob", "Frank", "David"],
        "Bob": ["Charlie", "Alice", "Heidi", "Ivan", "Eve", "Frank", "David", "Grace", "Julia"],
        "Charlie": ["Grace", "David", "Bob"],
        "David": ["Eve", "Charlie", "Frank", "Julia", "Grace", "Alice", "Bob", "Ivan", "Heidi"],
        "Eve": ["David", "Ivan", "Julia", "Frank", "Bob", "Heidi"],
        "Frank": ["Heidi", "David", "Alice", "Eve", "Julia", "Charlie", "Bob", "Ivan", "Grace"],
        "Grace": ["Charlie", "Ivan", "Heidi", "Julia", "David", "Bob", "Alice", "Frank", "Eve"],
        "Heidi": ["Frank", "Bob", "Grace"],
        "Ivan": ["Eve", "Grace", "Bob", "Julia", "Heidi", "Charlie", "Frank", "David", "Alice"],
        "Julia": ["Alice", "Heidi", "Ivan", "Charlie", "David", "Eve", "Frank", "Bob", "Grace"]
    }

    people = list(preference_lists.keys())

    print(f"We will evaluate {calculate_expected_pairing_count(len(people))} possible pairings.")
    
    optimal_result = find_optimal_pairing(people, preference_lists)
    print_pairing_results(optimal_result)

if __name__ == "__main__":
    main()