"""
AST Utilities for Configuration Parsing

This module provides utilities for parsing Python AST nodes to extract configuration
data from instantiated objects. The pattern is designed to be generic and reusable
across different configuration classes.

Pattern Overview:
================

The typical pattern for parsing configuration objects from AST is:

1. Use `extract_call_arguments()` to extract all keyword arguments from a function call
2. Pass the extracted arguments to the configuration class constructor
3. Let the configuration class handle validation and defaults via Pydantic

Example Usage:
==============

```python
from ..utils.ast_utils import extract_call_arguments
from ..queue import SQSQueueConfig

@node_visitor
def config_finder(self, node):
    if isinstance(node, ast.Call):
        # Check if this is a call to our configuration class
        if (isinstance(node.func, ast.Name) and 
            node.func.id == 'SQSQueueConfig'):
            
            # Extract all arguments
            args = extract_call_arguments(node)
            
            # Only process if we have required parameters
            if 'name' in args and not args['name'].startswith('__VARIABLE__:'):
                try:
                    # Create configuration instance with extracted arguments
                    config = SQSQueueConfig(**args)
                    self.configs.append(config)
                except Exception:
                    # Skip invalid configurations
                    pass
```

Benefits of This Pattern:
========================

1. **Scalability**: Adding new parameters to configuration classes requires zero
   changes to the parser - they're automatically handled.

2. **Type Safety**: Uses actual configuration class instances with full Pydantic
   validation and type checking.

3. **Maintainability**: Single generic parser instead of parameter-specific
   parsing logic.

4. **Robustness**: Handles complex expressions, variable references, and different
   Python versions gracefully.

5. **Consistency**: All parameters are processed the same way across all
   configuration classes.

Supported Value Types:
=====================

- Literal values (strings, numbers, booleans, None)
- Variable references (marked with `__VARIABLE__:` prefix)
- Python version compatibility (handles both < 3.8 and >= 3.8 AST structures)

Limitations:
============

- Complex expressions (arithmetic, function calls, etc.) are not supported
- Variable references cannot be resolved statically
- Only keyword arguments are extracted (positional args are ignored)
"""

import ast


def extract_ast_value(node):
    """
    Extract a value from an AST node, handling different Python versions.
    
    This function provides a unified interface for extracting values from AST nodes
    across different Python versions. It handles the transition from Python < 3.8
    to Python >= 3.8 where AST node structures changed.
    
    Args:
        node: The AST node to extract a value from
        
    Returns:
        The extracted value, or None if the node cannot be statically evaluated.
        Variable references are returned as `__VARIABLE__:{variable_name}`.
        
    Examples:
        >>> # String literal
        >>> extract_ast_value(ast.parse('"hello"').body[0].value)
        'hello'
        
        >>> # Boolean literal  
        >>> extract_ast_value(ast.parse('True').body[0].value)
        True
        
        >>> # Variable reference
        >>> extract_ast_value(ast.parse('my_var').body[0].value)
        '__VARIABLE__:my_var'
        
        >>> # Complex expression (not supported)
        >>> extract_ast_value(ast.parse('1 + 2').body[0].value)
        None
    """
    if isinstance(node, ast.Constant):
        return node.value
    elif isinstance(node, ast.Str):  # Python < 3.8
        return node.s
    elif isinstance(node, ast.Num):  # Python < 3.8
        return node.n
    elif isinstance(node, ast.NameConstant):  # Python < 3.8
        return node.value
    elif isinstance(node, ast.Name):
        # Handle variable references - we'll need to resolve these
        return f"__VARIABLE__:{node.id}"
    elif isinstance(node, ast.List):
        # Handle list literals
        return [extract_ast_value(item) for item in node.elts if extract_ast_value(item) is not None]
    elif isinstance(node, ast.Dict):
        # Handle dictionary literals
        result = {}
        for key, value in zip(node.keys, node.values):
            key_value = extract_ast_value(key)
            value_value = extract_ast_value(value)
            if key_value is not None and value_value is not None:
                result[key_value] = value_value
        return result
    elif isinstance(node, ast.Call):
        # Handle function calls like Path("directory")
        if isinstance(node.func, ast.Name) and node.func.id == 'Path':
            # Extract the first argument to Path()
            if node.args and len(node.args) == 1:
                return extract_ast_value(node.args[0])
        # For other function calls, we can't evaluate them statically
        return None
    else:
        # For complex expressions, we can't evaluate them statically
        return None


def extract_call_arguments(node: ast.Call) -> dict:
    """
    Extract all keyword arguments from an AST Call node.
    
    This function extracts all keyword arguments from a function call, making it
    easy to reconstruct configuration objects from their instantiation calls.
    It's designed to work with any configuration class that uses keyword arguments.
    
    Args:
        node: The AST Call node to extract arguments from
        
    Returns:
        Dictionary mapping argument names to their values. Variable references
        are included as `__VARIABLE__:{variable_name}`.
        
    Examples:
        >>> # Simple function call
        >>> tree = ast.parse('SQSQueueConfig(name="test", fifo=True)')
        >>> call_node = tree.body[0].value
        >>> extract_call_arguments(call_node)
        {'name': 'test', 'fifo': True}
        
        >>> # With variable reference
        >>> tree = ast.parse('SQSQueueConfig(name=queue_name, fifo=True)')
        >>> call_node = tree.body[0].value
        >>> extract_call_arguments(call_node)
        {'name': '__VARIABLE__:queue_name', 'fifo': True}
        
        >>> # Complex expression (filtered out)
        >>> tree = ast.parse('SQSQueueConfig(name="test", **kwargs)')
        >>> call_node = tree.body[0].value
        >>> extract_call_arguments(call_node)
        {'name': 'test'}  # **kwargs is skipped
    """
    args = {}
    
    for keyword in node.keywords:
        if keyword.arg:  # Skip **kwargs
            value = extract_ast_value(keyword.value)
            if value is not None:
                args[keyword.arg] = value
    
    return args


def node_visitor(f: callable) -> type[ast.NodeVisitor]:
    """
    A decorator that converts a function into an AST node visitor.

    Args:
        f: The function to convert into an AST node visitor.

    Returns:
        An AST node visitor.
    """
    class _NodeVisitor(ast.NodeVisitor):
        def visit(self, node):
            f(self, node)
            self.generic_visit(node)

    _NodeVisitor.__name__ = f.__name__
    _NodeVisitor.__doc__ = f.__doc__
    return _NodeVisitor