#!/usr/bin/env python3
"""Deep audit of all 20 ELA weeks - handles all edge cases."""
import ast
import os
import re
import sys
import subprocess
from collections import Counter
from typing import List, Dict, Tuple, Any, Optional

BASE = os.path.expanduser("~/Home_School/2nd_Grade/English_Language_Arts")

PAGE_TARGETS = {"Monday": 4, "Tuesday": 3, "Wednesday": 4, "Thursday": 4, "Friday": 3, "Teacher_Guide": 6}

def extract_words(generator_path: str) -> List[Tuple]:
    """Extract WORDS or words list from generator."""
    with open(generator_path) as f:
        source = f.read()
    
    # Try AST extraction
    try:
        tree = ast.parse(source)
        for node in ast.walk(tree):
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name) and target.id in ("WORDS", "words"):
                        if isinstance(node.value, (ast.List, ast.Tuple)):
                            words = []
                            for elt in node.value.elts:
                                if isinstance(elt, ast.Tuple):
                                    vals = []
                                    for val in elt.elts:
                                        if isinstance(val, ast.Constant):
                                            vals.append(val.value)
                                    if len(vals) >= 2:
                                        words.append(tuple(vals))
                            return words
    except:
        pass
    
    # Regex fallback - handle both WORDS and words
    m = re.search(r'^(\w+?_?WORDS?)\s*=\s*\[(.*?)\]', source, re.DOTALL | re.MULTILINE)
    if m:
        words = []
        for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\'](?:\s*,\s*["\']([^"\']*)["\'](?:\s*,\s*["\']([^"\']*)["\'])?)?\)', m.group(2)):
            vals = [tm.group(i) for i in range(1, 5) if tm.group(i) is not None]
            if vals:
                words.append(tuple(vals))
        return words
    return []

def extract_word_names(words: List[Tuple]) -> List[str]:
    return [w[0] for w in words]

def extract_story(source: str, day_key: str) -> str:
    """Extract story/reading text for a given day."""
    keys = {
        "Monday": [r'MONDAY_STORY\s*=\s*\((.*?)\)', r'MONDAY_STORY\s*=\s*"([^"]*)"'],
        "Thursday": [r'THURSDAY_STORY\s*=\s*\((.*?)\)', r'THURSDAY_STORY\s*=\s*"([^"]*)"'],
        "Friday": [r'FRIDAY_REVIEW_STORY\s*=\s*\((.*?)\)', r'FRIDAY_REVIEW_STORY\s*=\s*"([^"]*)"'],
        "Tuesday": [r'TUESDAY_READING\s*=\s*\((.*?)\)', r'TUESDAY_READING\s*=\s*"([^"]*)"'],
        "Wednesday": [r'WEDNESDAY_READING\s*=\s*\((.*?)\)', r'WEDNESDAY_READING\s*=\s*"([^"]*)"'],
    }
    for pat in keys.get(day_key, []):
        m = re.search(pat, source, re.DOTALL)
        if m:
            text = m.group(1).replace('\\n', ' ').replace('"', '').strip()
            return ' '.join(text.split())
    return ""

def check_words_in_story(story: str, word_names: List[str]) -> Tuple[List[str], List[str]]:
    """Check which words appear in a story (case-insensitive)."""
    if not story:
        return [], word_names
    found, missing = [], []
    story_lower = story.lower()
    for w in word_names:
        if w.lower() in story_lower:
            found.append(w)
        else:
            missing.append(w)
    return found, missing

def extract_swap_data(source: str) -> List[Dict]:
    """Extract swap data from both Tuesday and Thursday.
    
    Format: (original_sentence, (original_word1, replacement1), (original_word2, replacement2))
    Returns list of dicts with 'original_words' and 'replacement_words'.
    """
    swaps = []
    for prefix in ["TUESDAY_SWAP_DATA", "THURSDAY_SWAP_DATA"]:
        m = re.search(rf'{prefix}\s*=\s*\[(.*?)\]', source, re.DOTALL)
        if m:
            block = m.group(1)
            # Find each triple: (sentence, (w1, r1), (w2, r2))
            pattern = r'\(\s*["\']([^"\']*)["\']\s*,\s*\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)\s*,\s*\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)'
            for tm in re.finditer(pattern, block):
                swaps.append({
                    "sentence": tm.group(1),
                    "original_word1": tm.group(2),
                    "replacement1": tm.group(3),
                    "original_word2": tm.group(4),
                    "replacement2": tm.group(5),
                })
    return swaps

def extract_fill_data(source: str) -> List[Tuple]:
    fills = []
    for prefix in ["MONDAY_FILL_DATA", "MONDAY_FILL", "WEDNESDAY_FILL", "THURSDAY_FILL", "FRIDAY_FILL"]:
        m = re.search(rf'{prefix}\s*=\s*\[(.*?)\]', source, re.DOTALL)
        if m:
            for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)', m.group(1)):
                fills.append((tm.group(1), tm.group(2)))
    return fills

def extract_sort_answers(source: str) -> List[str]:
    """Extract all words from sort answers."""
    all_sorted = []
    for prefix in ["TEACHER_MONDAY_SORT_ANSWERS", "TEACHER_TUESDAY_SORT_ANSWERS",
                   "TEACHER_THURSDAY_SORT_ANSWERS", "TEACHER_WEDNESDAY_SORT_ANSWERS"]:
        m = re.search(rf'{prefix}\s*=\s*(\{{.*?\}})', source, re.DOTALL)
        if m:
            try:
                data = ast.literal_eval(m.group(1))
                for cat_words in data.values():
                    if isinstance(cat_words, list):
                        all_sorted.extend(cat_words)
            except:
                pass
    return all_sorted

def extract_pattern_hunt(source: str) -> Tuple[List, List]:
    """Extract pattern hunt words and match answers."""
    hunt_words = []
    hunt_matches = []
    
    for prefix in ["TUESDAY_PATTERN_HUNT_WORDS", "THURSDAY_PATTERN_HUNT_WORDS"]:
        m = re.search(rf'{prefix}\s*=\s*\[(.*?)\]', source, re.DOTALL)
        if m:
            for tm in re.finditer(r'["\']([^"\']*)["\']', m.group(1)):
                hunt_words.append(tm.group(1))
    
    for prefix in ["TEACHER_TUESDAY_PATTERN_HUNT", "TEACHER_THURSDAY_PATTERN_HUNT"]:
        m = re.search(rf'{prefix}\s*=\s*(\{{.*?\}})', source, re.DOTALL)
        if m:
            try:
                data = ast.literal_eval(m.group(1))
                if "match" in data:
                    hunt_matches.extend(data["match"])
                if "decoy" in data:
                    hunt_matches  # decoys exist
            except:
                pass
    
    return hunt_words, hunt_matches

def extract_correct_sentences(source: str) -> List[str]:
    sentences = []
    m = re.search(r'WEDNESDAY_CORRECT\s*=\s*\[(.*?)\]', source, re.DOTALL)
    if m:
        for tm in re.finditer(r'["\']([^"\']*)["\']', m.group(1)):
            sentences.append(tm.group(1))
    return sentences

def extract_paragraph_label_data(source: str) -> Tuple[List, List]:
    data, answers = [], []
    m = re.search(r'WEDNESDAY_PARAGRAPH_LABEL_DATA\s*=\s*\[(.*?)\]', source, re.DOTALL)
    if m:
        for tm in re.finditer(r'["\']([^"\']*)["\']', m.group(1)):
            data.append(tm.group(1))
    m = re.search(r'TEACHER_WEDNESDAY_PARAGRAPH_LABEL_ANSWERS\s*=\s*\[(.*?)\]', source, re.DOTALL)
    if m:
        for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)', m.group(1)):
            answers.append((tm.group(1), tm.group(2)))
    return data, answers

def extract_detective_answers(source: str) -> List[Tuple]:
    answers = []
    for prefix in ["TEACHER_WEDNESDAY_DETECTIVE_ANSWERS", "TEACHER_THURSDAY_DETECTIVE_ANSWERS"]:
        m = re.search(rf'{prefix}\s*=\s*\[(.*?)\]', source, re.DOTALL)
        if m:
            for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)', m.group(1)):
                answers.append((tm.group(1), tm.group(2)))
    return answers

def extract_opposite_words(source: str) -> Tuple[List, List]:
    words, answers = [], []
    m = re.search(r'FRIDAY_OPPOSITE_WORDS\s*=\s*\[(.*?)\]', source, re.DOTALL)
    if m:
        for tm in re.finditer(r'["\']([^"\']*)["\']', m.group(1)):
            words.append(tm.group(1))
    m = re.search(r'TEACHER_FRIDAY_OPPOSITE_ANSWERS\s*=\s*\[(.*?)\]', source, re.DOTALL)
    if m:
        for tm in re.finditer(r'\(\s*["\']([^"\']*)["\']\s*,\s*["\']([^"\']*)["\']\s*\)', m.group(1)):
            answers.append((tm.group(1), tm.group(2)))
    return words, answers

def count_pdf_pages(pdf_path: str) -> int:
    try:
        result = subprocess.run(["pdfinfo", pdf_path], capture_output=True, text=True, timeout=5)
        for line in result.stdout.split('\n'):
            if line.startswith("Pages:"):
                return int(line.split(":")[1].strip())
    except:
        pass
    return 0

def audit_week(week_num: int) -> Dict:
    issues = []
    week_dir = os.path.join(BASE, f"Week_{week_num:02d}")
    
    # Find generator
    generator = None
    for f in sorted(os.listdir(week_dir)):
        if f.startswith("generate_week") and f.endswith(".py"):
            if any(x in f for x in ["_backup", "_CORRUPTED", "_new", "_old"]):
                continue
            generator = os.path.join(week_dir, f)
            break
    
    if not generator:
        return {"week": week_num, "status": "FAIL", "issues": ["No generator found"], "page_counts": {}, "word_count": 0}
    
    result = {"week": week_num, "status": "PASS", "issues": [], "page_counts": {}, "word_count": 0}
    
    source = open(generator).read()
    
    # 1. Try running generator
    try:
        env = os.environ.copy()
        env["PYTHONPATH"] = BASE
        proc = subprocess.run(
            ["python3", os.path.basename(generator)],
            cwd=week_dir, capture_output=True, text=True, timeout=120, env=env
        )
        if proc.returncode != 0:
            err = proc.stderr.strip()[:300] if proc.stderr.strip() else proc.stdout.strip()[:300]
            issues.append(f"Generator exit code {proc.returncode}: {err[:200]}")
    except subprocess.TimeoutExpired:
        issues.append("Generator timed out (>120s)")
    except Exception as e:
        issues.append(f"Generator exception: {e}")
    
    # 2. Page counts (from existing PDFs)
    day_dirs = {}
    for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Teacher_Guide"]:
        day_subdir = os.path.join(week_dir, day)
        if os.path.isdir(day_subdir):
            pdfs = [f for f in os.listdir(day_subdir) if f.endswith('.pdf')]
            if pdfs:
                pages = count_pdf_pages(os.path.join(day_subdir, pdfs[0]))
                day_dirs[day] = {"pdfs": len(pdfs), "pages": pages}
    
    for day, info in day_dirs.items():
        target = PAGE_TARGETS.get(day, 0)
        result["page_counts"][day] = f"{info['pages']} pages (target: {target})"
        if info["pages"] > target:
            issues.append(f"{day}: {info['pages']} pages (target: {target}) - OVERFLOW")
        elif info["pages"] < target and target > 0:
            issues.append(f"{day}: {info['pages']} pages (target: {target}) - UNDER")
    
    # 3. Extract words
    words = extract_words(generator)
    word_names = extract_word_names(words)
    word_set = {w.lower() for w in word_names}
    result["word_count"] = len(words)
    
    if len(words) != 12:
        issues.append(f"Expected 12 words, found {len(words)}")
    
    # 4. Word coverage in stories
    for day_key in ["Monday", "Thursday", "Friday", "Tuesday", "Wednesday"]:
        story = extract_story(source, day_key)
        if story and words:
            found, missing = check_words_in_story(story, word_names)
            if missing:
                issues.append(f"{day_key} story missing words: {missing}")
    
    # 5. Swap words validation
    swaps = extract_swap_data(source)
    for sw in swaps:
        # Check original words are in word list (they should be - these are the words to swap OUT)
        if sw["original_word1"].lower() not in word_set:
            issues.append(f"Swap original word '{sw['original_word1']}' not in word list (sentence: {sw['sentence'][:40]}...)")
        if sw["original_word2"].lower() not in word_set:
            issues.append(f"Swap original word '{sw['original_word2']}' not in word list (sentence: {sw['sentence'][:40]}...)")
        # Check replacement words are in word list
        if sw["replacement1"].lower() not in word_set:
            issues.append(f"Swap replacement '{sw['replacement1']}' not in word list")
        if sw["replacement2"].lower() not in word_set:
            issues.append(f"Swap replacement '{sw['replacement2']}' not in word list")
        # Check for degenerate swaps (word replaced with itself)
        if sw["original_word1"].lower() == sw["replacement1"].lower():
            issues.append(f"Degenerate swap: '{sw['original_word1']}' → '{sw['replacement1']}' (same word)")
        if sw["original_word2"].lower() == sw["replacement2"].lower():
            issues.append(f"Degenerate swap: '{sw['original_word2']}' → '{sw['replacement2']}' (same word)")
    
    # 6. Sort coverage
    if words:
        all_sorted = extract_sort_answers(source)
        if all_sorted:
            freq = Counter(w.lower() for w in all_sorted)
            if len(all_sorted) != 12:
                issues.append(f"Sort answers cover {len(all_sorted)} words, expected 12")
            for w, c in freq.items():
                if c != 1:
                    issues.append(f"Sort word '{w}' appears {c} times (expected 1)")
            for w in word_names:
                if w.lower() not in freq:
                    issues.append(f"Word '{w}' not covered in sort answers")
    
    # 7. Pattern hunt has decoys
    hunt_words, hunt_matches = extract_pattern_hunt(source)
    if hunt_words and hunt_matches:
        if len(hunt_words) == len(hunt_matches):
            issues.append(f"Pattern hunt: all {len(hunt_matches)} words are targets (no decoys)")
    
    # 8. Correct sentences need capitalization
    correct_sents = extract_correct_sentences(source)
    for sent in correct_sents:
        if sent and sent[0].isupper():
            issues.append(f"Correct sentence already capitalized: '{sent[:50]}'")
    
    # 9. Paragraph label counts
    pl_data, pl_answers = extract_paragraph_label_data(source)
    if pl_data and pl_answers:
        if len(pl_data) != len(pl_answers):
            issues.append(f"Paragraph labels: {len(pl_data)} data items vs {len(pl_answers)} answers")
    
    # 10. Data format mismatches (tuple repr rendering)
    if 'TEACHER_TUESDAY_SCRAMBLE_ANSWERS' in source:
        m = re.search(r'TEACHER_TUESDAY_SCRAMBLE_ANSWERS\s*=\s*\[(.*?)\]', source, re.DOTALL)
        if m:
            # Check that it's proper Python tuple format, not repr
            if not re.search(r'\(\s*["\']', m.group(1)):
                issues.append("Scramble answers may have tuple repr format issue")
    
    # 11. Wednesday detective types - check for valid context clue types
    detective_answers = extract_detective_answers(source)
    valid_context_clue_types = {"Definition", "Example", "Contrast", "Description", "Restatement", "Inference"}
    valid_verb_types = {"Action", "Linking", "Helping", "Auxiliary", "Transitive", "Intransitive"}
    valid_noun_types = {"Compound", "Common", "Proper", "Collective", "Abstract", "Concrete", "Singular", "Plural"}
    all_valid = valid_context_clue_types | valid_verb_types | valid_noun_types
    for _, ans_type in detective_answers:
        if ans_type.strip() and ans_type.strip() not in all_valid:
            issues.append(f"Detective answer type '{ans_type}' is non-standard")
    
    # 12. Friday opposite words in word list
    opp_words, opp_answers = extract_opposite_words(source)
    if opp_words and word_set:
        for w in opp_words:
            if w.lower() not in word_set:
                issues.append(f"Friday opposite word '{w}' not in word list")
    
    # 13. Fill answers in word list
    fills = extract_fill_data(source)
    if fills and word_set:
        for sentence, answer in fills:
            if answer.lower() not in word_set:
                issues.append(f"Fill answer '{answer}' not in word list")
    
    # 14. Check for duplicate swap data (Tuesday == Thursday)
    tue_swaps = [s for s in swaps if any('TUESDAY' in source[:source.find(str(s["sentence"]))] for _ in [1])]
    # Simplified: just check if same sentences appear in both TUESDAY_SWAP and THURSDAY_SWAP
    tue_block = re.search(r'TUESDAY_SWAP_DATA\s*=\s*\[(.*?)\]', source, re.DOTALL)
    thu_block = re.search(r'THURSDAY_SWAP_DATA\s*=\s*\[(.*?)\]', source, re.DOTALL)
    if tue_block and thu_block:
        tue_sents = re.findall(r'\(\s*"([^"]+)"', tue_block.group(1))
        thu_sents = re.findall(r'\(\s*"([^"]+)"', thu_block.group(1))
        common = set(tue_sents) & set(thu_sents)
        if common:
            issues.append(f"Swap data: {len(common)} identical sentence(s) in Tuesday AND Thursday")
    
    result["issues"] = issues
    result["status"] = "FAIL" if issues else "PASS"
    return result

def main():
    print("=" * 80)
    print("ELA WEEK DEEP AUDIT - ALL 20 WEEKS")
    print("=" * 80)
    
    all_results = []
    total_issues = 0
    
    for week_num in range(1, 21):
        result = audit_week(week_num)
        all_results.append(result)
        issues = result["issues"]
        total_issues += len(issues)
        
        print(f"\n{'─'*60}")
        print(f"Week {week_num}: {result['status']} (words: {result['word_count']})")
        print(f"{'─'*60}")
        
        if issues:
            for i, issue in enumerate(issues, 1):
                print(f"  {i}. {issue}")
        
        pc = result.get("page_counts", {})
        if pc:
            print(f"  Pages: {', '.join(f'{k}={v}' for k, v in pc.items())}")
    
    print(f"\n{'='*80}")
    print(f"AUDIT SUMMARY")
    print(f"{'='*80}")
    
    passed = sum(1 for r in all_results if r["status"] == "PASS")
    failed = sum(1 for r in all_results if r["status"] == "FAIL")
    
    print(f"\n  PASS: {passed}/20")
    print(f"  FAIL: {failed}/20")
    print(f"  Total issues: {total_issues}")
    
    # Issue categories
    cat_counts = Counter()
    issue_categories = {
        "Generator Error": lambda x: "Generator" in x,
        "Word Count": lambda x: "Expected 12 words" in x,
        "Story Word Coverage": lambda x: "story missing words" in x,
        "Swap Word Not in List": lambda x: "Swap" in x and "not in word list" in x,
        "Degenerate Swap": lambda x: "Degenerate swap" in x,
        "Swap Data Duplicate": lambda x: "identical sentence" in x,
        "Sort Coverage": lambda x: "Sort" in x.lower() and ("expected 12" in x or "not covered" in x or "appears" in x),
        "Pattern Hunt No Decoys": lambda x: "no decoys" in x,
        "Already Capitalized": lambda x: "already capitalized" in x.lower(),
        "Paragraph Label Mismatch": lambda x: "Paragraph labels" in x,
        "Detective Type": lambda x: "Detective" in x,
        "Opposite Not in List": lambda x: "opposite" in x.lower() and "not in word list" in x,
        "Fill Answer Not in List": lambda x: "Fill answer" in x,
        "Page Count Overflow": lambda x: "OVERFLOW" in x,
        "Page Count Under": lambda x: "UNDER" in x,
    }
    
    for r in all_results:
        for issue in r["issues"]:
            matched = False
            for cat, check_fn in issue_categories.items():
                if check_fn(issue):
                    cat_counts[cat] += 1
                    matched = True
                    break
            if not matched:
                cat_counts["Other"] += 1
    
    print(f"\n  Issue breakdown:")
    for cat, count in cat_counts.most_common():
        print(f"    {cat}: {count}")
    
    print(f"\n  Failed weeks detail:")
    for r in all_results:
        if r["status"] == "FAIL":
            print(f"    Week {r['week']}: {len(r['issues'])} issues")
            for issue in r["issues"]:
                print(f"      - {issue}")

if __name__ == "__main__":
    main()