import re
import ast
import os
from typing import List, Tuple, Optional

def extract_code_from_markdown(md_text: str) -> Optional[str]:
    """
    Finds the most relevant code block. Uses greedy matching to handle nested blocks 
    (e.g. a Markdown plan containing smaller code snippets).
    """
    # Greedy match to capture outermost block if nested
    pattern = r"```(?:\w+)?\s*\n?([\s\S]*)```"
    match = re.search(pattern, md_text)
    
    if match:
        return match.group(1).strip()
    
    # Fallback to non-greedy findall if greedy failed
    pattern_fallback = r"```(?:\w+)?\s*(.*?)```"
    matches = re.findall(pattern_fallback, md_text, re.DOTALL)
    if not matches:
        return None

    candidates = []
    
    # Analyze matches to score them
    for content in matches:
        lines = content.strip().splitlines()
        line_count = len(lines)
        
        # Heuristic: Detect shell commands
        is_shell = False
        first_word = lines[0].strip().split()[0] if lines and lines[0].strip() else ""
        if first_word.lower() in ["pip", "python", "npm", "cd", "ls", "nova", "git", "bash", "sh"]:
            is_shell = True
            
        candidates.append({
            "content": content.strip(),
            "lines": line_count,
            "is_shell": is_shell
        })

    # Filter: If we have multiple blocks, discard short shell blocks
    if len(candidates) > 1:
        filtered = [c for c in candidates if not (c["is_shell"] and c["lines"] < 5)]
        if filtered:
            candidates = filtered

    # Sort by length (descending) - Assume the actual code is the largest chunk
    candidates.sort(key=lambda x: x["lines"], reverse=True)

    return candidates[0]["content"]

def parse_multiple_files(text: str) -> List[Tuple[str, str]]:
    """
    Parses text for multiple [CREATE: filename] ... content pairs.
    
    Robustness: 
    1. Handles bolding/whitespace in tags.
    2. FIRST tries to find a Markdown code block.
    3. FALLBACK: If no code block is found, captures text but STOPS at conversational markers.
    
    Args:
        text (str): The text to parse for [CREATE: filename] ... content pairs.
    
    Returns:
        List[Tuple[str, str]]: A list of tuples containing the filename and content.
    """
    files_to_create: List[Tuple[str, str]] = []
    
    # 1. Split text by the [CREATE: filename] tag
    # This divides the text into [preamble, filename1, content1, filename2, content2...]
    split_pattern = r"(?:\*\*|__)?\[CREATE:\s*(.*?)\s*\](?:\*\*|__)?"
    segments = re.split(split_pattern, text, flags=re.IGNORECASE)
    
    if len(segments) < 3:
        return []

    # Iterate starting from index 1 (first filename), taking steps of 2
    for i in range(1, len(segments), 2):
        filename = segments[i].strip()
        following_text = segments[i+1]
        
        # Clean up filename (remove potential trailing punctuation)
        filename = filename.rstrip(".:,")
        
        # Strategy A: Look for explicit Markdown Code Block (Preferred)
        match = re.search(r"```(?:\w+)?\s*\n?(.*)```", following_text)
        
        if match:
            code = match.group(1).strip()
            # Clean up in case of trailing backticks from splitting artifacts
            if code.endswith("```"):
                code = code[:-3].strip()
                
            files_to_create.append((filename, code))
        else:
            # Strategy B (Fallback): Take the raw text but filter garbage
            # Stop if we hit typical conversational markers to prevent "Explanation: ..." from being written
            lines = following_text.strip().split('\n')
            clean_lines = []
            
            for line in lines:
                # Stop markers (Common conversational starts)
                stripped = line.strip()
                if (stripped.startswith("**") or 
                    stripped.startswith("Error:") or 
                    stripped.startswith("Note:") or 
                    stripped.startswith("To fix this") or
                    stripped.startswith("Here is") or
                    stripped.startswith("I have")):
                    break
                clean_lines.append(line)
            
            code = "\n".join(clean_lines).strip()
            if code:
                files_to_create.append((filename, code))
            
    return files_to_create

def check_syntax(content: str, filename: str) -> Tuple[bool, str]:
    """
    Verifies if the code content is valid syntax (Python only).
    Returns (is_valid, error_message).
    """
    if not filename.endswith(".py"):
        return True, "" # Skip checks for non-python files
        
    try:
        ast.parse(content)
        return True, ""
    except SyntaxError as e:
        return False, f"Line {e.lineno}: {e.msg}"
    except Exception as e:
        return False, str(e)

def generate_ast_map(startpath: str) -> str:
    """
    Scans the directory and generates a high-level map of the code structure
    (Classes and Functions) using AST, skipping bodies to save tokens.
    """
    repo_map = []
    
    excluded_dirs = {
        ".git", "__pycache__", "venv", "env", "node_modules", 
        ".idea", ".vscode", "dist", "build", ".next", ".ds_store", 
        ".mypy_cache", ".nova"
    }

    for root, dirs, files in os.walk(startpath):
        # Filter directories in-place
        dirs[:] = [d for d in dirs if d not in excluded_dirs and not d.startswith(".")]
        
        for file in files:
            if not file.endswith(".py"):
                continue
                
            full_path = os.path.join(root, file)
            # Standardize path separators
            rel_path = os.path.relpath(full_path, startpath).replace("\\", "/")
            
            try:
                with open(full_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                    if not content.strip(): continue
                    tree = ast.parse(content)
                
                repo_map.append(f"FILE: {rel_path}")
                
                for node in tree.body:
                    if isinstance(node, ast.ClassDef):
                        repo_map.append(f"  class {node.name}")
                        for sub in node.body:
                            if isinstance(sub, ast.FunctionDef) and not sub.name.startswith("_"):
                                repo_map.append(f"    def {sub.name}")
                    elif isinstance(node, ast.FunctionDef):
                        repo_map.append(f"  def {node.name}")
                    elif isinstance(node, ast.AsyncFunctionDef):
                        repo_map.append(f"  async def {node.name}")
                        
            except Exception:
                repo_map.append(f"FILE: {rel_path} (Parse Error)")
                
    return "\n".join(repo_map)

def strip_ansi(text: str) -> str:
    """Aggressive cleaner that removes ANSI and broken terminal fragments."""
    if not text: return ""
    import re
    # 1. Standard ANSI Escape codes
    ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
    text = ansi_escape.sub('', text)
    
    # 2. Fix broken fragments (like [1m or [0m appearing as literal text)
    text = re.sub(r'\[\d+(?:\;\d+)*m', '', text)
    
    # 3. Remove Carriage Returns (\r) which cause line overwriting/backspacing artifacts
    text = text.replace('\r', '')
    
    # 4. Keep only printable characters to ensure the UI stays clean
    return "".join(ch for ch in text if ch.isprintable() or ch in "\n\t")