import typing
import enum
import random


def _process_annotation(encoded_annotation, *, ns=None):
    """
    Process annotation encoded the same way as in the descriptions of existing plans.
    Returns reference to the annotation (type object) and the list temporary types
    that needs to be deleted once the processing (parameter validation) is complete.

    Parameters
    ----------
    encoded_annotation : dict
        Dictionary that contains encoded annotation. The dictionary may contain the following
        keys: ``type`` - type represented as a string, ``devices`` - a dictionary that maps
        device types and lists of device names, ``plans`` - similar dictionary for plan types
        and ``enums`` - similar dictionary for simple enums (strings).
    ns : dict
        Namespace that is used to evaluate the annotations. Custom types with unique names
        may be added to the namesace by the function.

    Returns
    -------
    type
        Type reconstructed from annotation.
    dict
        Namespace dictionary with created types.
    """
    # Namespace that contains the types created in the process of processing the annotation
    ns = ns or {}
    if "typing" not in ns:
        ns.update({"typing": typing})
    if "enum" not in ns:
        ns.update({"enum": enum})
    if "NoneType" not in ns:
        ns.update({"NoneType": type(None)})

    annotation_type_str = "<not-specified>"
    annotation_type = None

    try:
        if "type" not in encoded_annotation:
            raise ValueError("Type is not specififed")

        # Verify that the annotation does not have any unsupported keys
        allowed_keys = ("type", "devices", "plans", "enums")
        invalid_keys = []
        for k in encoded_annotation:
            if k not in allowed_keys:
                invalid_keys.append(k)
        if invalid_keys:
            raise ValueError(
                f"Annotation contains unsupported keys: {invalid_keys}. Supported keys: {allowed_keys}"
            )

        annotation_type_str = encoded_annotation.get("type")  # Required!
        devices = encoded_annotation.get("devices", {})
        plans = encoded_annotation.get("plans", {})
        enums = encoded_annotation.get("enums", {})
        items = devices.copy()
        items.update(plans)
        items.update(enums)

        # Create types
        for item_name in items:
            # The dictionary value for the devices/plans may be a list(tuple) of items
            #   or None. If the value is None, then any device or plan will be accepted.
            #   The list of device or plan names will be used to construct enum.Enum
            #   type, which is used to verify if the name in args or kwargs matches
            #   one of the arguments.
            if items[item_name] is not None:
                # Create temporary unique type name
                type_name = type_name_base = item_name
                while True:
                    # Try the original name first
                    if type_name not in ns:
                        break
                    type_name = f"{type_name_base}_{random.randint(0, 100000)}_"

                # Replace all occurrences of the type nae in the custom annotation.
                annotation_type_str = annotation_type_str.replace(item_name, type_name)

                # Create temporary type as a subclass of enum.Enum, e.g.
                # enum.Enum('Device', {'det1': 'det1', 'det2': 'det2', 'det3': 'det3'})
                type_code = f"enum.Enum('{type_name}', {{"
                for d in items[item_name]:
                    type_code += f"'{d}': '{d}',"
                type_code += "})"
                ns[type_name] = eval(type_code, ns, ns)

            else:
                # Accept any device or plan: any string will be accepted without
                #   verification.
                annotation_type_str = annotation_type_str.replace(item_name, "str")

        # Once all the types are created,  execute the code for annotation.
        annotation_type = eval(annotation_type_str, ns, ns)

    except Exception as ex:
        raise TypeError(f"Failed to process annotation '{annotation_type_str}': {ex}'")

    return annotation_type, ns