import re
from typing import Any, Dict

def get_indexed_data(data_name: str, generated_data_by_node: Dict[str, Any]) -> Any:
    # Get the index/key from the data name
    start_index = data_name.index('[')
    end_index = data_name.index(']')
    index = data_name[start_index + 1:end_index]
    data_name = data_name[:start_index]

    # Get the data from the generated data dictionary
    data = generated_data_by_node.get(data_name)
    if isinstance(data, (list, tuple)):
        return data[int(index)]
    elif isinstance(data, dict):
        return data[index]
    else:
        raise AssertionError(f"Could not get data for {data_name}. Indexes for type {type(data)} are not supported.")

def escape_str_for_json(value_as_str: str):
    # Replace newlines with escaped newlines
    value_as_str = value_as_str.replace(r"\n", "\\n")

    # Escape double quotes inside the string
    value_as_str = value_as_str.replace('"', '\\"')
    return value_as_str

def unescape_str_in_json(value_as_str: str):
    # Replace escaped newlines with newlines
    value_as_str = value_as_str.replace("\\n", r"\n")

    # Replace escaped double quotes with double quotes
    value_as_str = value_as_str.replace('\\"', '"')
    return value_as_str

def replace_generated_data_by_node(match_obj, generated_data_by_node: Dict[str, Any]) -> str:
    # Get the data name from the matched object
    data_name = match_obj.group(1)

    # Check if the data name has an index/key
    if '[' in data_name and ']' in data_name:
        value = get_indexed_data(data_name, generated_data_by_node)
    else:
        value = generated_data_by_node[data_name]

    value_as_str = str(value)
    value_as_str = escape_str_for_json(value_as_str)

    return value_as_str

def inject_node_data(commands_data_str: str, node_id, generated_data_by_node: dict[str, Any]):
    pattern = rf"__&{node_id}\.(\w+)__"
    return re.sub(pattern, lambda m: replace_generated_data_by_node(m, generated_data_by_node), commands_data_str)

def nullify_all_data_references(commands_data_str: str):
    """Replaces all __&i.data__ by null."""
    pattern = r"__&(\d+)\.(\w+(?:\[\d+\])*)__"
    return re.sub(pattern, "null", commands_data_str)

def find_data_references_indices(commands_data_str: str) -> dict[int, dict[str, list[tuple]]]:
    """
    Returns a dictionary with the ID of each command and the name of the generated data, along with their start and end positions
    in the raw commands data string.

    Args:
        commands_data_str (str): A string containing the data to create a graph/command.

    Returns:
        A dictionary where the keys are command IDs and the values are dictionaries representing the generated data for that
        command. These inner dictionaries have keys that are names of generated data variables and values that are lists of tuples.
        Each tuple in these lists contains the starting and ending positions of the generated data variable within the
        commands_data_str input string.

    Example:
        {1: {'urls[0]': [(115, 129), (299, 313)]}, 2: {'text': [(214, 225)]}}
    """
    pattern = r"__&(\d+)\.(\w+(?:\[\d+\])*)__"
    matches = re.finditer(pattern, commands_data_str)
    references = {}
    for match in matches:
        command_id = int(match.group(1))
        generated_data_name = match.group(2)
        start_index = match.start()
        end_index = match.end()

        if command_id not in references:
            references[command_id] = {}

        if generated_data_name not in references[command_id]:
            references[command_id][generated_data_name] = []

        references[command_id][generated_data_name].append((start_index, end_index))

    return references
