```python
def group_batches(jobs: list, capacity: int) -> list:
    """
    Groups jobs into batches based on group constraints, priority, and weight capacity.
    
    Args:
        jobs: A list of [name, group, priority, weight] quadruples.
        capacity: The weight capacity of one batch.
        
    Returns:
        A list of batches, where each batch is a list of job names in the order they were taken.
        
    Raises:
        ValueError: If input validation fails.
    """
    # Validate capacity
    if not isinstance(capacity, int) or isinstance(capacity, bool) or capacity <= 0:
        raise ValueError("Capacity must be a positive integer.")
    
    # Validate jobs argument
    if not isinstance(jobs, list):
        raise ValueError("Jobs must be a list.")
    
    # Validate individual jobs and collect metadata
    seen_names = set()
    # Structure to track job state: {name: {group, priority, weight, index}}
    job_map = {}
    # Track the queue of jobs per group to maintain arrival order
    group_queues = {}
    
    for i, job in enumerate(jobs):
        if not isinstance(job, list):
            raise ValueError(f"Job at index {i} is not a list.")
        if len(job) != 4:
            raise ValueError(f"Job at index {i} does not have 4 elements.")
        
        name, group, priority, weight = job
        
        # Validate name
        if not isinstance(name, str) or len(name) == 0:
            raise ValueError(f"Job at index {i} has an invalid name.")
        
        # Validate group
        if not isinstance(group, str) or len(group) == 0:
            raise ValueError(f"Job at index {i} has an invalid group.")
        
        # Validate priority
        if not isinstance(priority, int) or isinstance(priority, bool):
            raise ValueError(f"Job at index {i} has an invalid priority.")
        
        # Validate weight
        if not isinstance(weight, int) or isinstance(weight, bool):
            raise ValueError(f"Job at index {i} has an invalid weight.")
        if weight < 1 or weight > capacity:
            raise ValueError(f"Job at index {i} has a weight out of range [1, {capacity}].")
        
        # Check for duplicate names
        if name in seen_names:
            raise ValueError(f"Duplicate job name: {name}")
        seen_names.add(name)
        
        job_info = {
            'name': name,
            'group': group,
            'priority': priority,
            'weight': weight,
            'index': i
        }
        job_map[name] = job_info
        
        if group not in group_queues:
            group_queues[group] = []
        group_queues[group].append(job_info)
    
    # Initialize scheduling state
    # eligible_heads: list of job_info objects that are currently the head of their group
    eligible_heads = []
    for group, queue in group_queues.items():
        if queue:
            eligible_heads.append(queue[0])
    
    batches = []
    
    while eligible_heads:
        # Create a new batch
        batch = []
        remaining_capacity = capacity
        
        # Sort eligible heads by priority descending, then by index ascending (earlier arrival)
        # Note: The problem says "breaking ties by earlier arrival". 
        # Since we process groups in