from typing import Optional
from typing import List
from typing import Tuple
from typing import Set


def parse_node_types(node_type_specs: Optional[str]) -> List[Tuple[Set[str], Optional[float]]]:
    """
    Parse a specification for zero or more node types.

    Takes a comma-separated list of node types. Each node type is a
    slash-separated list of at least one instance type name (like 'm5a.large'
    for AWS), and an optional bid in dollars after a colon.

    Raises ValueError if a node type cannot be parsed.

    Inputs should look something like this:

    >>> parse_node_types('c5.4xlarge/c5a.4xlarge:0.42,t2.large')
    [({'c5.4xlarge', 'c5a.4xlarge'}, 0.42), ({'t2.large'}, None)]

    :param node_type_specs: A string defining node types

    :returns: a list of node types, where each type is the set of
              instance types, and the float bid, or None.
    """

    # Collect together all the node types
    parsed = []

    if node_type_specs:
        # Some node types were actually specified
        for node_type_spec in node_type_specs.split(','):
            try:
                # Types are comma-separated
                # Then we have the colon and the bid
                parts = node_type_spec.split(':')

                if len(parts) > 2:
                    # Only one bid allowed
                    raise ValueError(f'Cound not parse node type "{node_type_spec}": multiple bids')

                # Instance types are slash-separated within an equivalence
                # class
                instance_types = set(parts[0].split('/'))

                for instance_type in instance_types:
                    if instance_type == '':
                        # No empty instance types allowed
                        raise ValueError(f'Cound not parse node type "{node_type_spec}": empty instance type')

                # Build the node type tuple
                parsed.append((instance_types, float(parts[1]) if len(parts) > 1 else None))
            except Exception as e:
                if isinstance(e, ValueError):
                    raise
                else:
                    raise ValueError(f'Cound not parse node type "{node_type_spec}"')

    return parsed