#!/usr/bin/env python3
"""
TinyTorch Student Notebook Generator

Transforms complete implementation notebooks into student exercise versions.
Uses special markers to identify what becomes student exercises.

Usage:
    python bin/generate_student_notebooks.py --module tensor
    python bin/generate_student_notebooks.py --all
"""

import argparse
import json
import re
from pathlib import Path
from typing import Dict, List, Tuple, Any
import sys

class NotebookGenerator:
    """Transforms complete notebooks into student exercise versions with nbgrader support."""
    
    def __init__(self, use_nbgrader=False):
        self.use_nbgrader = use_nbgrader
        self.markers = {
            # TinyTorch markers (existing)
            'exercise_start': '#| exercise_start',
            'exercise_end': '#| exercise_end', 
            'hint': '#| hint:',
            'solution_test': '#| solution_test:',
            'difficulty': '#| difficulty:',
            'keep_imports': '#| keep_imports',
            'remove_cell': '#| remove_cell',
            
            # nbgrader markers (new)
            'nbgrader_solution_begin': '### BEGIN SOLUTION',
            'nbgrader_solution_end': '### END SOLUTION',
            'nbgrader_hidden_tests_begin': '### BEGIN HIDDEN TESTS',
            'nbgrader_hidden_tests_end': '### END HIDDEN TESTS'
        }
    
    def process_notebook(self, notebook_path: Path) -> Dict[str, Any]:
        """Transform a complete notebook into student version."""
        print(f"📝 Processing: {notebook_path}")
        
        with open(notebook_path, 'r') as f:
            notebook = json.load(f)
        
        processed_cells = []
        
        for cell in notebook['cells']:
            processed_cell = self._process_cell(cell)
            if processed_cell:  # None means remove cell
                processed_cells.append(processed_cell)
        
        notebook['cells'] = processed_cells
        return notebook
    
    def _process_cell(self, cell: Dict[str, Any]) -> Dict[str, Any]:
        """Process a single notebook cell with both TinyTorch and nbgrader support."""
        if cell['cell_type'] != 'code':
            return cell  # Keep markdown cells as-is
        
        source_lines = cell['source']
        if not source_lines:
            return cell
        
        # Check for remove_cell marker
        if any(self.markers['remove_cell'] in line for line in source_lines):
            return None  # Remove this cell
        
        # Process nbgrader solution blocks
        if any(self.markers['nbgrader_solution_begin'] in line for line in source_lines):
            return self._transform_nbgrader_cell(cell)
        
        # Check for TinyTorch exercise markers
        if any(self.markers['exercise_start'] in line for line in source_lines):
            return self._transform_exercise_cell(cell)
        
        # Check for keep_imports marker
        if any(self.markers['keep_imports'] in line for line in source_lines):
            return self._clean_markers(cell)
        
        return cell
    
    def _transform_nbgrader_cell(self, cell: Dict[str, Any]) -> Dict[str, Any]:
        """Transform nbgrader solution blocks for student version."""
        source_lines = cell['source']
        new_lines = []
        
        in_solution = False
        in_hidden_tests = False
        placeholder_added = False
        
        for line in source_lines:
            if self.markers['nbgrader_solution_begin'] in line:
                in_solution = True
                placeholder_added = False
                if self.use_nbgrader:
                    new_lines.append(line)  # Keep marker for nbgrader
                    # Add placeholder immediately after BEGIN SOLUTION
                    new_lines.append("    # YOUR CODE HERE\n")
                    new_lines.append("    raise NotImplementedError()\n")
                    placeholder_added = True
                continue
            elif self.markers['nbgrader_solution_end'] in line:
                in_solution = False
                if self.use_nbgrader:
                    new_lines.append(line)  # Keep marker for nbgrader
                continue
            elif self.markers['nbgrader_hidden_tests_begin'] in line:
                in_hidden_tests = True
                if self.use_nbgrader:
                    new_lines.append(line)  # Keep marker for nbgrader
                continue
            elif self.markers['nbgrader_hidden_tests_end'] in line:
                in_hidden_tests = False
                if self.use_nbgrader:
                    new_lines.append(line)  # Keep marker for nbgrader
                continue
            elif in_solution:
                # Skip solution lines (placeholder already added)
                continue
            elif in_hidden_tests:
                # Keep hidden tests for nbgrader, remove for regular students
                if self.use_nbgrader:
                    new_lines.append(line)
                # Skip for regular students
                continue
            else:
                # Keep non-solution lines
                new_lines.append(line)
        
        cell['source'] = new_lines
        return cell

    def _transform_exercise_cell(self, cell: Dict[str, Any]) -> Dict[str, Any]:
        """Transform a cell with exercise markers into student version."""
        source_lines = cell['source']
        new_lines = []
        
        in_exercise = False
        exercise_header_lines = []  # Store function def, docstring etc.
        hints = []
        solution_tests = []
        difficulty = "medium"
        
        for line in source_lines:
            if self.markers['exercise_start'] in line:
                in_exercise = True
                continue
            elif self.markers['exercise_end'] in line:
                in_exercise = False
                # Add the preserved header + exercise placeholder
                new_lines.extend(exercise_header_lines)
                new_lines.extend(self._create_exercise_placeholder(hints, solution_tests, difficulty))
                # Reset for next exercise
                exercise_header_lines = []
                hints = []
                solution_tests = []
                difficulty = "medium"
                continue
            elif self.markers['hint'] in line:
                hint = line.split(self.markers['hint'], 1)[1].strip()
                hints.append(hint)
                continue
            elif self.markers['solution_test'] in line:
                test = line.split(self.markers['solution_test'], 1)[1].strip()
                solution_tests.append(test)
                continue
            elif self.markers['difficulty'] in line:
                difficulty = line.split(self.markers['difficulty'], 1)[1].strip()
                continue
            elif in_exercise:
                # Preserve function signature and docstring, skip implementation
                if self._is_function_signature_or_docstring(line):
                    exercise_header_lines.append(line)
                # Skip implementation lines (but keep signature/docstring)
                continue
            else:
                # Keep non-exercise lines
                new_lines.append(line)
        
        cell['source'] = new_lines
        return cell

    def _is_function_signature_or_docstring(self, line: str) -> bool:
        """Check if line is part of function signature or docstring."""
        stripped = line.strip()
        
        # Empty lines
        if not stripped:
            return False
            
        # Function definition
        if (stripped.startswith('def ') or 
            stripped.startswith('class ') or
            stripped.startswith('@')):  # decorators
            return True
            
        # Function signature continuation (parameters on multiple lines) 
        if (stripped.endswith(',') or 
            stripped.endswith('\\') or
            stripped.startswith(')') or
            '->' in stripped):
            return True
            
        # Docstrings (triple quotes)
        if ('"""' in stripped or "'''" in stripped):
            return True
            
        # Docstring content (common patterns)
        if (stripped.startswith('Args:') or 
            stripped.startswith('Returns:') or
            stripped.startswith('Raises:') or
            stripped.startswith('Note:') or
            stripped.startswith('Example:')):
            return True
            
        # Implementation code (skip these)
        if (stripped.startswith('self.') or
            stripped.startswith('if ') or
            stripped.startswith('elif ') or
            stripped.startswith('else:') or
            stripped.startswith('for ') or
            stripped.startswith('while ') or
            stripped.startswith('return ') or
            stripped.startswith('raise ') or
            stripped.startswith('try:') or
            stripped.startswith('except ') or
            stripped.startswith('with ') or
            '=' in stripped and not stripped.startswith('"""') and not stripped.startswith("'''")):
            return False
            
        # Comments (keep them as they might be part of docstring)
        if stripped.startswith('#'):
            return True
            
        # If we're not sure and it's just text, assume it's docstring content
        # This catches parameter descriptions, etc.
        return True
    
    def _create_exercise_placeholder(self, hints: List[str], tests: List[str], difficulty: str) -> List[str]:
        """Create TODO placeholder for students."""
        lines = []
        
        # Add difficulty indicator and description
        difficulty_emoji = {"easy": "🟢", "medium": "🟡", "hard": "🔴"}
        lines.append(f"    # {difficulty_emoji.get(difficulty, '🟡')} TODO: Implement this method ({difficulty})\n")
        
        # Add hints
        for hint in hints:
            lines.append(f"    # HINT: {hint}\n")
        
        # Add test guidance
        for test in tests:
            lines.append(f"    # TEST: {test}\n")
        
        lines.append("    \n")
        lines.append("    # Your implementation here\n")
        
        if self.use_nbgrader:
            lines.append("    # YOUR CODE HERE\n")
            lines.append("    raise NotImplementedError()\n")
        else:
            lines.append("    pass\n")
        
        return lines
    
    def _clean_markers(self, cell: Dict[str, Any]) -> Dict[str, Any]:
        """Remove generator markers from cell."""
        source_lines = cell['source']
        cleaned_lines = []
        
        for line in source_lines:
            # Skip marker lines
            if any(marker in line for marker in self.markers.values()):
                continue
            cleaned_lines.append(line)
        
        cell['source'] = cleaned_lines
        return cell
    
    def save_student_notebook(self, notebook: Dict[str, Any], output_path: Path):
        """Save the student version notebook."""
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        with open(output_path, 'w') as f:
            json.dump(notebook, f, indent=2)
        
        print(f"✅ Student version saved: {output_path}")

def main():
    parser = argparse.ArgumentParser(description="Generate student exercise notebooks")
    parser.add_argument('--module', type=str, help='Generate for specific module')
    parser.add_argument('--all', action='store_true', help='Generate for all modules')
    parser.add_argument('--output-suffix', default='_student', help='Suffix for student notebooks')
    parser.add_argument('--nbgrader', action='store_true', help='Generate nbgrader-compatible notebooks')
    
    args = parser.parse_args()
    
    if not args.module and not args.all:
        parser.error("Must specify either --module or --all")
    
    generator = NotebookGenerator(use_nbgrader=args.nbgrader)
    modules_dir = Path("modules")
    
    if args.module:
        modules = [args.module]
    else:
        modules = [d.name for d in modules_dir.iterdir() if d.is_dir()]
    
    for module in modules:
        module_dir = modules_dir / module
        dev_notebook = module_dir / f"{module}_dev.ipynb"
        
        if not dev_notebook.exists():
            print(f"⚠️  No dev notebook found for {module}: {dev_notebook}")
            continue
        
        # Generate student version
        notebook = generator.process_notebook(dev_notebook)
        
        if args.nbgrader:
            output_path = module_dir / f"{module}_assignment.ipynb"
            generator.save_student_notebook(notebook, output_path)
        else:
            output_path = module_dir / f"{module}{args.output_suffix}.ipynb"
            generator.save_student_notebook(notebook, output_path)

if __name__ == "__main__":
    main() 