import os
import re
from dotmap import DotMap
from .common import filepath_or_string, GlobalVars


def extract_placeholders(s: str) -> set:
    '''Extracts placeholder variables in the format `{variable}` from
    an unformatted template string.'''

    # Regex pattern to match placeholders with alphanumerics, dots, and underscores.
    placeholder_pattern = r'\{([\w\.]+)\}'
    
    # Find all matches in the string.
    matches = re.findall(placeholder_pattern, s)
    
    # Return the set of distinct placeholders.
    return set(matches)


def find_cyclical_placeholders(s: str, _parents: tuple = None, _cycles: set = None, **kwargs) -> set[tuple]:
    '''Recursively interpolates supplied kwargs into a template string to validate
    that there are no cyclical dependencies that would cause infinite recursion.
    
    Returns a list of paths (expressed as tuples of nodes) of cyclical placeholders.

    # Example

    ```python
    kwargs = {'a': '1', 'b': '2', 'c': '{d}+{e}', 'd': '3', 'e': '{c}'}
    s = '{a} + {b} = {c}'
    find_cyclical_placeholders(s, **kwargs)

    >>> {('c', 'e', 'c')}
    ```
    '''

    # Track the lineage of each placeholder so we can see if it is its own ancestor.
    if _parents is None:
        _parents = tuple()

    # Keep track of any cycles encountered.
    if _cycles is None:
        _cycles = set()

    # Extract the placeholders from the input.
    placeholders = extract_placeholders(s)

    # Recursion will naturally end once there are no more nested placeholders.
    for p in placeholders:

        # Any placeholder that has itself in its ancestry forms a cycle.
        if p in _parents:
            _cycles.add(_parents + (p,))
        
        # For placeholders that are not their own ancestor, recursively 
        # interpolate the kwargs into the nested placeholders until we reach
        # strings without placeholders.
        else:
            find_cyclical_placeholders(
                ('{'+p+'}').format(**kwargs),
                _parents = _parents+(p,),
                _cycles = _cycles,
                **kwargs
            )

    return _cycles


def format_html_template(template: str, **kwargs) -> str:
    '''Interpolates variables specified as keyword arguments 
    into the given HTML template.
    '''

    # Load the template if a filepath is given.
    template = filepath_or_string(template)

    # Ensure the template does not have cyclical placeholder references.
    cycles = find_cyclical_placeholders(template, globalvars = GlobalVars(), **kwargs)

    if len(cycles) > 0:
        raise ValueError('Template has cyclical dependencies: {cycles}')

    # Iteratively interpolate global variables and the kwargs into the template until
    # there are no more placeholders. The loop is used to account for nested template references.
    formatted_html = template
    while len(extract_placeholders(formatted_html)) > 0:
        formatted_html = formatted_html.format(globalvars = GlobalVars(), **kwargs)

    # Return the formatted HTML.
    return formatted_html


def map_templates(dir: str, parent = '') -> DotMap:
    '''Recursively maps the templates directory into a nested dict structure.
    Leaves map the filestems of .html template files to their contents.
    '''

    output = {}

    # List the files and subdirectories at the top level.
    for sub in os.listdir(os.path.join(parent,dir)):

        # Construct the full path to the file or subdir from the root of the tree.
        full_path = os.path.join(parent,dir,sub)

        # Recursively map subdirectories.
        if os.path.isdir(full_path):
            output[sub] = map_templates(sub, parent = dir)
            continue

        # Templates must be .html files.
        filestem, ext = os.path.splitext(sub)
        if ext != '.html':
            continue

        # Load template file.
        with open(full_path, 'r') as file:
            html = file.read()

        output[filestem] = html
            
    return DotMap(output)