"""Week Generator Scanner.

Extracts word lists, phonetic claims, and content data from week generators.
Returns structured data for the audit system to validate.
"""

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


def _normalize_pattern(pattern: str) -> str:
    """Normalize generator pattern labels to match DB pattern codes.
    
    Generator labels use formats like:
      - 'long u (silent e)', 'long a (ai)', 'oh sound (ow)'
      - 'or = /or/', 'a_e = /eɪ/'
      - 're + turn', 'dis + agree'
      - 'zz = short i', 'dd = short a'
      - 'ck sound (ck)', 'ch sound (ch)'
    
    DB codes use formats like:
      - 'long_u_cvc e', 'long_a_ai', 'long_o_ow'
      - 'r_control_or', 'long_a_cvc e'
      - 'prefix_re', 'prefix_dis'
      - 'double_consonant'
      - 'digraph_ck', 'digraph_ch'
    """
    p = pattern.strip().lower()
    
    # --- Silent E patterns ---
    # 'long X (silent e)' -> 'long_X_cvc e'
    m = re.match(r'long\s+(a|e|i|o|u)\s*\(silent\s*e\)', p)
    if m:
        v = m.group(1)
        # Special: 'relieve' uses 'long e (silent e)' but actual pattern is 'long_e_ee' (ie team)
        return f"long_{v}_cvc e"
    
    # 'long X (Y)' -> 'long_X_Y' (vowel teams)
    m = re.match(r'long\s+(a|e|i|o|u)\s*\((\w+)\)', p)
    if m:
        vowel, team = m.group(1), m.group(2)
        return f"long_{vowel}_{team}"
    
    # --- Sound patterns ---
    # 'X sound (YZ)' -> digraph/soft/r_control code
    m = re.match(r'(\w+)\s+sound\s*\((\w+)\)', p)
    if m:
        sound, letters = m.group(1), m.group(2)
        # Digraphs
        if letters in ('sh', 'ch', 'th', 'wh', 'ph', 'ck'):
            return f"digraph_{letters}"
        # Vowel teams
        if letters in ('oa', 'oo', 'ow', 'ou', 'ew', 'ai', 'ay', 'ee', 'ea', 'ie'):
            # ow/ou can be long or short
            if sound == 'oh' and letters == 'ow':
                return 'long_o_ow'
            if sound == 'ow' and letters == 'ou':
                return 'short_o_ou'
            if sound == 'ew':
                return 'long_u_ew'
            return f"long_{letters[0]}_{letters}"
        # Soft c/g
        if letters in ('ce', 'ci', 'cy', 'ge', 'gi', 'gy'):
            return f"soft_{letters[0]}_{letters}"
    
    # --- R-controlled patterns ---
    # 'X = /Y/' where X is a vowel+r combo
    m = re.match(r'(ar|er|ir|or|ur)\s*=\s*/', p)
    if m:
        return f"r_control_{m.group(1)}"
    
    # 'X_Y = /Z/' (e.g., 'a_e = /eɪ/')
    m = re.match(r'([aeiou])_([aeiou])\s*=\s*/', p)
    if m:
        return f"long_{m.group(1)}_cvc e"
    
    # 'ea = short e / ...' complex patterns
    if 'ea = short e' in p:
        return 'short_e'
    
    # --- Prefix/suffix patterns ---
    # 'X + Y' -> prefix_X or suffix_X
    m = re.match(r'([a-z]+)\s*\+\s*', p)
    if m:
        part = m.group(1)
        if part in ('un', 're', 'dis', 'im', 'in', 'mis', 'non', 'pre', 'over', 'under'):
            return f"prefix_{part}"
    
    # 'Y + X' where X is a suffix (at end)
    m = re.search(r'\+\s*(es|ed|ing|er|est|ly|ful|less|ment|ion|able|ive)\s*$', p)
    if m:
        return f"suffix_{m.group(1)}"
    
    # --- Double consonant patterns ---
    # 'XX = short Y' -> double_consonant
    m = re.match(r'([bcdfghjklmnpqrstvwxyz])\1\s*=\s*short\s*[aeiou]', p)
    if m:
        return "double_consonant"
    
    # --- Homophone patterns ---
    # 'word1/word2' or 'word1, word2'
    if '/' in p and len(p.split('/')) >= 2:
        parts = p.split('/')
        if all(len(part.strip()) >= 2 for part in parts):
            return "homophone"
    
    return pattern


class WordClaim:
    """A single word with its claimed pattern from a week generator."""
    def __init__(self, word: str, claimed_pattern: str, definition: str,
                 week: str, line: int = 0):
        self.word = word.strip().lower()
        self.claimed_pattern = _normalize_pattern(claimed_pattern.strip())
        self.definition = definition.strip()
        self.week = week
        self.line = line

    def __repr__(self):
        return f"WordClaim({self.word!r}, {self.claimed_pattern!r}, week={self.week!r})"


class WeekData:
    """All extracted data from a single week generator."""
    def __init__(self, week_num: int):
        self.week_num = week_num
        self.phonics_label: str = ""
        self.word_claims: List[WordClaim] = []
        self.story_words: List[Tuple[str, str]] = []  # (word, context)
        self.errors: List[str] = []


def scan_week(week_dir: str) -> Optional[WeekData]:
    """Scan a week directory and extract all word claims.
    
    Args:
        week_dir: Path to Week_XX directory
        
    Returns:
        WeekData with all extracted claims, or None if no generator found
    """
    # Find the main generator file (not backup, not corrupted)
    generator = _find_generator(week_dir)
    if not generator:
        return None
    
    data = WeekData(int(re.search(r"Week_(\d+)", week_dir).group(1))
                    if re.search(r"Week_(\d+)", week_dir) else 0)
    
    try:
        source = open(generator).read()
        data.phonics_label = _extract_phonics_label(source)
        data.word_claims = _extract_words(source, f"Week_{data.week_num:02d}")
        data.story_words = _extract_story_words(source)
    except Exception as e:
        data.errors.append(f"Failed to parse {generator}: {e}")
    
    return data


def scan_all_weeks(base_dir: str) -> List[WeekData]:
    """Scan all week directories.
    
    Args:
        base_dir: Path to English_Language_Arts root
        
    Returns:
        List of WeekData for each week that has content
    """
    results = []
    for entry in sorted(os.listdir(base_dir)):
        if entry.startswith("Week_"):
            week_path = os.path.join(base_dir, entry)
            if os.path.isdir(week_path):
                data = scan_week(week_path)
                if data and data.word_claims:
                    results.append(data)
    return results


def _find_generator(week_dir: str) -> Optional[str]:
    """Find the main generator file for a week."""
    candidates = []
    for f in os.listdir(week_dir):
        if f.startswith("generate_week") and f.endswith(".py"):
            # Skip backup, corrupted, and new files
            if any(x in f for x in ["_backup", "_CORRUPTED", "_new"]):
                continue
            candidates.append(os.path.join(week_dir, f))
    
    # Prefer non-_new version
    candidates.sort(key=lambda x: (1 if "_new" in os.path.basename(x) else 0, x))
    return candidates[0] if candidates else None


def _extract_phonics_label(source: str) -> str:
    """Extract PHONICS_LABEL from generator source."""
    match = re.search(r'PHONICS_LABEL\s*=\s*["\']([^"\']+)["\']', source)
    return match.group(1) if match else ""


def _extract_words(source: str, week: str) -> List[WordClaim]:
    """Extract WORDS list from generator source.
    
    Handles both formats:
    - WORDS = [("word", "pattern", "definition"), ...]  (standard)
    - WORDS = [("word", "definition", "example"), ...]  (homophone weeks)
    
    For homophone weeks (detected by PHONICS_LABEL or pattern content),
    the second field is a definition, not a pattern.
    """
    claims = []
    
    # Find WORDS = [...] or words = [...] block (case-insensitive)
    words_match = re.search(
        r'^(?:WORDS|words)\s*=\s*\[(.*?)\]',
        source,
        re.DOTALL | re.MULTILINE
    )
    
    if not words_match:
        return claims
    
    words_block = words_match.group(1)
    
    # Detect if this is a homophone week (3-tuple with definition, not pattern)
    is_homophone_week = _detect_homophone_week(source, words_block)
    
    # Parse each tuple (supports 2-4 elements: word, pattern, definition, [notes])
    tuple_pattern = re.compile(
        r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*(?:,\s*["\']([^"\']*)["\']\s*)?(?:,\s*["\']([^"\']*)["\']\s*)?\)'
    )
    
    for match in tuple_pattern.finditer(words_block):
        word = match.group(1).strip()
        pattern = match.group(2).strip()
        definition = match.group(3).strip() if match.group(3) else ""
        
        # For homophone weeks, the second field is a definition, not a pattern
        if is_homophone_week:
            pattern = "homophone"
            # Definition is already in group(2), swap
            definition = pattern if not definition else definition
        
        claims.append(WordClaim(
            word=word,
            claimed_pattern=pattern,
            definition=definition,
            week=week,
        ))
    
    return claims


def _detect_homophone_week(source: str, words_block: str) -> bool:
    """Detect if a week uses (word, definition, example) format instead of (word, pattern, definition).
    
    Heuristics:
    - PHONICS_LABEL or topic_label mentions "homophone"
    - The second field is a long phrase (> 15 chars, multiple words)
    - Pattern field doesn't match known pattern formats
    """
    # Check PHONICS_LABEL or topic_label
    for label_key in ['PHONICS_LABEL', 'topic_label', 'TOPIC_LABEL']:
        label_match = re.search(rf'{label_key}\s*=\s*["\']([^"\']+)["\']', source)
        if label_match and 'homophone' in label_match.group(1).lower():
            return True
    
    # Check first tuple's second field
    first_tuple = re.search(r'\(\s*["\']([^"\']+)["\']\s*,\s*["\']([^"\']+)["\']', words_block)
    if first_tuple:
        second_field = first_tuple.group(2).lower()
        # If second field is a long phrase, it's likely a definition
        words_in_field = len(second_field.split())
        if len(second_field) > 15 and words_in_field >= 3:
            # Check if it looks like a pattern (short technical label)
            # Patterns are typically < 4 words and contain technical terms
            pattern_keywords = ['silent e', 'long a', 'long e', 'long i', 'long o', 'long u',
                               'short a', 'short e', 'short i', 'short o', 'short u',
                               'r control', 'digraph', 'soft c', 'soft g', 'double',
                               'prefix', 'suffix', 'homophone', 'irregular']
            # Also check standalone technical terms
            technical_terms = ['cvc', 'ai', 'ay', 'ea', 'oa', 'oo', 'ow', 'ou', 'ew',
                              'ce', 'ci', 'cy', 'ge', 'gi', 'gy', 'ck', 'sh', 'ch', 'th', 'wh']
            
            has_pattern_kw = any(kw in second_field for kw in pattern_keywords)
            # For technical terms, require them to be standalone or at word boundaries
            has_tech = any(re.search(rf'\b{term}\b', second_field) for term in technical_terms)
            
            if not (has_pattern_kw or has_tech):
                return True
    
    return False


def _extract_story_words(source: str) -> List[Tuple[str, str]]:
    """Extract words used in <strong> tags from stories.
    
    Returns list of (word, surrounding_context) tuples.
    """
    words = []
    
    # Find all <strong>word</strong> patterns
    for match in re.finditer(r'<strong>([^<]+)</strong>', source):
        word = match.group(1).strip()
        # Get surrounding context
        start = max(0, match.start() - 30)
        end = min(len(source), match.end() + 30)
        context = source[start:end].replace('\n', ' ')
        words.append((word, context))
    
    return words


def extract_all_words_from_content(source: str) -> set:
    """Extract all English words from story text (for cross-reference).
    
    Strips HTML tags and extracts word tokens.
    """
    # Remove HTML tags
    text = re.sub(r'<[^>]+>', '', source)
    # Extract word tokens (2+ letters)
    tokens = re.findall(r'\b[a-zA-Z]{2,}\b', text)
    return {t.lower() for t in tokens}
