import csv
import json
import os
import random
import shutil
import subprocess
import sys
import tempfile
import shlex
import webbrowser
from datetime import datetime
import logging

from kubelingo.utils.ui import Fore, Style, yaml, humanize_module
import questionary
from kubelingo.utils.config import (
    ROOT,
    LOGS_DIR,
    HISTORY_FILE,
    DEFAULT_DATA_FILE,
    YAML_QUESTIONS_FILE,
    DATA_DIR,
    INPUT_HISTORY_FILE,
    VIM_HISTORY_FILE,
    KILLERCODA_CSV_FILE,
    ENABLED_QUIZZES,
    VIM_QUESTIONS_FILE,
    KUBECTL_BASIC_SYNTAX_QUIZ_FILE,
    KUBECTL_OPERATIONS_QUIZ_FILE,
    KUBECTL_RESOURCE_TYPES_QUIZ_FILE,
)

try:
    from prompt_toolkit import PromptSession
    from prompt_toolkit.history import FileHistory
except ImportError:
    PromptSession = None
    FileHistory = None

from kubelingo.modules.base.session import StudySession, SessionManager
from pathlib import Path
import os

# AI-based validator for command equivalence


from kubelingo.modules.base.loader import load_session
from kubelingo.modules.json_loader import JSONLoader
from kubelingo.modules.md_loader import MDLoader
from kubelingo.modules.yaml_loader import YAMLLoader
from dataclasses import asdict
from kubelingo.utils.validation import commands_equivalent
# Existing import
# Existing import
from .vim_yaml_editor import VimYamlEditor
from .answer_checker import evaluate_transcript
from kubelingo.sandbox import spawn_pty_shell, launch_container_sandbox, ShellResult, StepResult
import logging  # for logging in exercises
# Stub out AI evaluator to avoid heavy external dependencies


def _get_quiz_files():
    """Returns a list of paths to JSON command quiz files, excluding special ones."""
    json_dir = os.path.join(DATA_DIR, 'json')
    if not os.path.isdir(json_dir):
        return []

    # Exclude special files that have their own quiz modes or are enabled in the main menu
    excluded_files = {os.path.basename(f) for f in ENABLED_QUIZZES.values() if f}
    if YAML_QUESTIONS_FILE:
        excluded_files.add(os.path.basename(YAML_QUESTIONS_FILE))

    return sorted([
        os.path.join(json_dir, f)
        for f in os.listdir(json_dir)
        if f.endswith('.json') and f not in excluded_files
    ])


def _get_md_quiz_files():
    """Returns a list of paths to Markdown quiz files that contain runnable questions."""
    md_dir = os.path.join(DATA_DIR, 'md')
    if not os.path.isdir(md_dir):
        return []

    runnable_files = []
    for f in os.listdir(md_dir):
        if f.endswith(('.md', '.markdown')):
            file_path = os.path.join(md_dir, f)
            # Pass exit_on_error=False to prevent halting on non-quiz markdown files.
            questions = load_questions(file_path, exit_on_error=False)
            # A file is a runnable quiz if it has at least one question of a runnable type.
            if any(q.get('type') in ('command', 'live_k8s', 'live_k8s_edit') for q in questions):
                runnable_files.append(file_path)

    return sorted(runnable_files)


def _get_yaml_quiz_files():
    """Returns a list of paths to YAML quiz files."""
    yaml_dir = os.path.join(DATA_DIR, 'yaml')
    if not os.path.isdir(yaml_dir):
        return []
    return sorted([
        os.path.join(yaml_dir, f)
        for f in os.listdir(yaml_dir)
        if f.endswith(('.yaml', '.yml'))
    ])


def get_all_flagged_questions():
    """Returns a list of all questions from all files that are flagged for review."""
    # Only YAML-based quizzes are used for review
    all_quiz_files = _get_yaml_quiz_files()
    if os.path.exists(VIM_QUESTIONS_FILE):
        all_quiz_files.append(VIM_QUESTIONS_FILE)
    all_quiz_files = sorted(set(all_quiz_files))

    all_flagged = []
    for f in all_quiz_files:
        try:
            qs = load_questions(f, exit_on_error=False)
        except Exception:
            continue
        # The `review: true` flag in the source YAML is the source of truth.
        for q in qs:
            if q.get('review'):
                q['data_file'] = f
                all_flagged.append(q)
    return all_flagged


def _clear_all_review_flags(logger):
    """Removes 'review' flag from all questions in all known YAML quiz files."""
    if yaml is None:
        logger.error("Cannot clear review flags: PyYAML is not installed.")
        print(f"{Fore.RED}Cannot clear review flags: PyYAML is not installed.{Style.RESET_ALL}")
        return

    all_quiz_files = _get_yaml_quiz_files()
    if os.path.exists(VIM_QUESTIONS_FILE):
        all_quiz_files.append(VIM_QUESTIONS_FILE)
    all_quiz_files = sorted(set(all_quiz_files))

    cleared_count = 0
    for file_path in all_quiz_files:
        try:
            with open(file_path, 'r') as f:
                # Use list(yaml.safe_load_all(f)) if file can have multiple docs
                questions = yaml.safe_load(f)
        except Exception:
            continue  # Skip files that can't be read

        if not isinstance(questions, list):
            continue

        changed = False
        for q in questions:
            if isinstance(q, dict) and 'review' in q:
                del q['review']
                changed = True
        
        if changed:
            cleared_count += 1
            try:
                with open(file_path, 'w') as f:
                    yaml.safe_dump(questions, f, sort_keys=False, allow_unicode=True)
            except Exception as e:
                logger.error(f"Failed to save cleared review flags for {file_path}: {e}")
    
    if cleared_count > 0:
        print(f"\n{Fore.GREEN}Cleared all review flags.{Style.RESET_ALL}")
    else:
        print(f"\n{Fore.YELLOW}No flagged questions found to clear.{Style.RESET_ALL}")


def check_dependencies(*commands):
    """Check if all command-line tools in `commands` are available."""
    missing = []
    for cmd in commands:
        if not shutil.which(cmd):
            missing.append(cmd)
    return missing

def load_questions(data_file, exit_on_error=True):
    """Loads questions from JSON, YAML, or Markdown files using dedicated loaders."""
    ext = os.path.splitext(data_file)[1].lower()
    # Handle raw JSON list-of-modules questions format (from Markdown/YAML quizzes)
    if ext == '.json':
        try:
            with open(data_file, 'r', encoding='utf-8') as f:
                raw_data = json.load(f)
        except Exception as e:
            if exit_on_error:
                print(Fore.RED + f"Error loading quiz data from {data_file}: {e}" + Style.RESET_ALL)
                sys.exit(1)
            return []
        # If JSON file is a list, detect format:
        if isinstance(raw_data, list):
            # Case: list of modules with nested prompts (e.g., CKAD exercises)
            if raw_data and isinstance(raw_data[0], dict) and 'prompts' in raw_data[0]:
                questions = []
                for module in raw_data:
                    if not isinstance(module, dict):
                        continue
                    category = module.get('category')
                    for prompt in module.get('prompts', []):
                        if not isinstance(prompt, dict):
                            continue
                        q = prompt.copy()
                        if 'question_type' in q:
                            q['type'] = q['question_type']
                        if category is not None:
                            q['category'] = category
                        questions.append(q)
                return questions
            # Case: list of simple questions (e.g., Killercoda CKAD)
            if raw_data and isinstance(raw_data[0], dict) and 'prompt' in raw_data[0]:
                questions = []
                for item in raw_data:
                    if not isinstance(item, dict):
                        continue
                    q = item.copy()
                    # Normalize 'answer' key to 'response'
                    if 'answer' in q:
                        q['response'] = q.pop('answer')
                    questions.append(q)
                return questions
        loader = JSONLoader()
    elif ext in ('.md', '.markdown'):
        loader = MDLoader()
    elif ext in ('.yaml', '.yml'):
        loader = YAMLLoader()
    else:
        if exit_on_error:
            print(Fore.RED + f"Unsupported file type for quiz data: {data_file}" + Style.RESET_ALL)
            sys.exit(1)
        return []

    try:
        # Loaders return a list of Question objects. We'll convert them to dicts.
        questions_obj = loader.load_file(data_file)

        # If a loader returns a list containing a single list of questions (a common
        # scenario for flat JSON files), flatten it to a simple list of questions.
        if questions_obj and len(questions_obj) == 1 and isinstance(questions_obj[0], list):
            questions_obj = questions_obj[0]

        # The fields of the Question dataclass need to be compatible with what the
        # rest of this module expects. We convert them to dicts.
        questions = []
        for q_obj in questions_obj:
            q_dict = asdict(q_obj)
            # Ensure a default question type for loaders, to match inline parsing
            if 'type' not in q_dict or not q_dict['type']:
                q_dict['type'] = 'command'
            # Ensure response is populated from answer if present, for compatibility
            q_dict['response'] = q_dict.get('response', '') or q_dict.get('answer', '')
            # Populate response from validation command if no explicit response provided
            if not q_dict['response'] and q_dict.get('validation_steps'):
                first_val = q_dict['validation_steps'][0]
                cmd = first_val.get('cmd') if isinstance(first_val, dict) else getattr(first_val, 'cmd', '')
                if cmd:
                    q_dict['response'] = cmd.strip()
            # Promote common metadata fields, including nested 'metadata' (e.g., from YAML)
            meta = q_dict.get('metadata', {}) or {}
            nested = meta.get('metadata') if isinstance(meta.get('metadata'), dict) else {}
            for fld in ('citation', 'source', 'category', 'response', 'validator'):
                val = meta.get(fld) or nested.get(fld)
                if val:
                    q_dict[fld] = val
            # Extract YAML authoring/editing canonical answers for validation
            if 'answer' in meta:
                q_dict['answer'] = meta.get('answer')
            if 'correct_yaml' in meta:
                q_dict['correct_yaml'] = meta.get('correct_yaml')
            questions.append(q_dict)
        return questions
    except Exception as e:
        if exit_on_error:
            print(Fore.RED + f"Error loading quiz data from {data_file}: {e}" + Style.RESET_ALL)
            sys.exit(1)
        return []

    
class NewSession(StudySession):
    """A study session for all Kubernetes-related quizzes."""

    def __init__(self, logger):
        super().__init__(logger)
        self.cluster_name = None
        self.kubeconfig_path = None
        self.region = None
        self.creds_acquired = False
        self.live_session_active = False # To control cleanup logic
        self.kind_cluster_created = False
    
    def _run_one_exercise(self, question: dict):
        """
        Run a single live_k8s exercise by prompting for commands and running an assertion script.
        This supports unit tests for live mode.
        """
        # Prepare the assert script
        import tempfile, os, shlex, subprocess
        # Write the assert script to a temp file using context manager (for mocks)
        path = None
        try:
            with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.sh') as tf:
                tf.write(question.get('assert_script', ''))
                path = tf.name
            os.chmod(path, 0o700)
            # Prompt user for commands until 'done'
            sess = PromptSession(history=None)
            while True:
                cmd = sess.prompt()
                if cmd is None or cmd.strip().lower() == 'done':
                    break
                subprocess.run(shlex.split(cmd), capture_output=True, text=True, check=False)
            # Run the assertion script
            result = subprocess.run(['bash', path], capture_output=True, text=True)
            status = 'correct' if result.returncode == 0 else 'incorrect'
            self.logger.info(f"Live exercise: prompt=\"{question.get('prompt')}\" result=\"{status}\"")
        finally:
            if path:
                try:
                    os.remove(path)
                except Exception:
                    pass

    def initialize(self):
        """Basic initialization. Live session initialization is deferred."""
        return True

    def _build_interactive_menu_choices(self):
        """Helper to construct the list of choices for the interactive menu."""
        # Discover all quiz files from JSON, MD, and YAML sources.
        all_quiz_files = _get_quiz_files() + _get_md_quiz_files() + _get_yaml_quiz_files()
        
        # Explicitly remove enabled quizzes from the "other" list to avoid duplication.
        enabled_quiz_stems = {Path(p).stem for p in ENABLED_QUIZZES.values()}
        all_quiz_files = [p for p in all_quiz_files if Path(p).stem not in enabled_quiz_stems]
        all_quiz_files = sorted(list(set(all_quiz_files)))

        all_flagged = get_all_flagged_questions()
        
        choices = []

        # 1. Add enabled quizzes from config
        for name, path in ENABLED_QUIZZES.items():
            if os.path.exists(path):
                choices.append({"name": name, "value": path})
            else:
                choices.append({"name": name, "value": f"{path}_disabled", "disabled": "Not available"})

        # 2. Review Flagged
        review_text = "Review Flagged Questions"
        if all_flagged:
            review_text = f"Review {len(all_flagged)} Flagged Questions"
        choices.append({"name": review_text, "value": "review"})

        # 3. View Session History
        choices.append({"name": "View Session History", "value": "view_history"})
        
        # 4. Help
        choices.append({"name": "Help", "value": "help"})

        # 5. Exit App
        choices.append({"name": "Exit App", "value": "exit_app"})
        return choices, all_flagged

        # 5. Session Type (disabled)
        choices.append({"name": "Session Type (PTY/Docker)", "value": "session_type_disabled", "disabled": "Selection simplified"})
        
        # 6. Custom Quiz (disabled)
        choices.append({"name": "Custom Quiz", "value": "custom_quiz_disabled", "disabled": "Coming soon"})
        
        choices.append(questionary.Separator("Other Quizzes (Coming Soon)"))
        
        if all_quiz_files:
            seen_subjects = set()
            # List additional quizzes; enable selected ones by default
            enabled_files = {"kubectl_common_operations.yaml", "resource_reference.yaml"}
            for file_path in all_quiz_files:
                base = os.path.basename(file_path)
                name = os.path.splitext(base)[0]
                subject = humanize_module(name).strip()
                if subject in seen_subjects:
                    continue
                seen_subjects.add(subject)
                # Enable certain YAML quizzes, others are coming soon
                if base in enabled_files:
                    choices.append({"name": subject, "value": file_path})
                else:
                    choices.append({
                        "name": subject,
                        "value": file_path,
                        "disabled": "Not yet implemented"
                    })
        
        if all_flagged:
            choices.append(questionary.Separator())
            choices.append({"name": f"Clear All {len(all_flagged)} Review Flags", "value": "clear_flags"})
        
        return choices, all_flagged

    def _show_static_help(self):
        """Displays a static, hardcoded help menu as a fallback."""
        print(f"\n{Fore.CYAN}--- Kubelingo Help ---{Style.RESET_ALL}\n")
        print("This screen provides access to all quiz modules and application features.\n")

        print(f"{Fore.GREEN}Vim Quiz{Style.RESET_ALL}")
        print("  The primary, active quiz module for practicing Vim commands.\n")

        print(f"{Fore.GREEN}Kubectl Commands{Style.RESET_ALL}")
        print("  Test your knowledge of kubectl operations.\n")

        print(f"{Fore.GREEN}Review Flagged Questions{Style.RESET_ALL}")
        print("  Starts a quiz session with only the questions you have previously flagged for review.")
        print("  Use this for focused study on topics you find difficult.\n")

        print(f"{Fore.GREEN}View Session History{Style.RESET_ALL}")
        print("  Displays a summary of your past quiz sessions, including scores and timings.\n")

        print(f"{Fore.GREEN}Help{Style.RESET_ALL}")
        print("  Shows this help screen.\n")

        print(f"{Fore.GREEN}Exit App{Style.RESET_ALL}")
        print("  Quits the application.\n")

        print(f"{Fore.YELLOW}Other Options (Not yet implemented){Style.RESET_ALL}")
        print(f"  - {Style.DIM}Session Type (PTY/Docker){Style.RESET_ALL}")
        print(f"  - {Style.DIM}Custom Quiz{Style.RESET_ALL}")
        print(f"  - {Style.DIM}Other Quizzes...{Style.RESET_ALL}")
        print(f"  - {Style.DIM}Clear All Review Flags{Style.RESET_ALL} (appears when questions are flagged)")

    def _show_help(self):
        """
        Displays a dynamically generated help menu using an AI model to summarize
        the available quizzes and features.
        """
        # This approach ensures the help text stays up-to-date with the application's
        # capabilities without requiring manual updates to hardcoded strings.

        if not os.getenv('OPENAI_API_KEY'):
            print(f"\n{Fore.YELLOW}AI-powered help requires an OpenAI API key.{Style.RESET_ALL}")
            print(f"{Fore.YELLOW}Set the OPENAI_API_KEY environment variable to enable it.{Style.RESET_ALL}")
            print(f"{Fore.CYAN}Falling back to static help text.{Style.RESET_ALL}")
            self._show_static_help()
            return

        try:
            import llm

            # Discover available quizzes by building the menu choices.
            choices, _ = self._build_interactive_menu_choices()
            
            # Separate choices into enabled quizzes and other features.
            enabled_quizzes = [
                c['name'] for c in choices 
                if isinstance(c, dict) and 'disabled' not in c 
                and Path(str(c.get('value', ''))).exists()
            ]
            
            disabled_features = [c['name'] for c in choices if isinstance(c, dict) and 'disabled' in c]

            prompt = (
                "You are the friendly help system for a command-line learning tool called 'kubelingo'.\n"
                "Your task is to generate a concise, user-friendly help screen based on the following available features.\n"
                "Present the information clearly, using simple Markdown for formatting.\n\n"
                "Active, usable quizzes:\n"
                f"- {', '.join(enabled_quizzes)}\n\n"
                "Standard features:\n"
                "- Review Flagged Questions: For focused study on difficult topics.\n"
                "- View Session History: To see past performance.\n"
                "- Help: To show this screen.\n"
                "- Exit App: To quit the application.\n\n"
                "Features that are listed in the menu but are not yet implemented (disabled):\n"
                f"- {', '.join(disabled_features)}\n\n"
                "Generate a help text that explains these options to the user."
            )

            print(f"\n{Fore.CYAN}--- Kubelingo Help (AI Generated) ---{Style.RESET_ALL}\n")
            print(f"{Fore.YELLOW}Generating dynamic help with AI...{Style.RESET_ALL}")

            # Get the default model, which should be configured via `llm` CLI or env vars.
            model = llm.get_model()
            response = model.prompt(prompt, system="You are a helpful assistant for a CLI tool.")
            
            print(f"\n{response.text}")

        except (ImportError, Exception) as e:
            print(f"\n{Fore.RED}AI-powered help generation failed: {e}{Style.RESET_ALL}")
            print(f"{Fore.CYAN}Falling back to static help text.{Style.RESET_ALL}")
            self._show_static_help()

    def run_exercises(self, args):
        """
        Router for running exercises. It decides which quiz to run.
        """
        # All exercises now run through the unified quiz runner.
        self._run_unified_quiz(args)

    def _run_command_quiz(self, args):
        """Attempt Rust bridge execution first; fallback to Python if unavailable or fails."""
        try:
            from kubelingo.bridge import rust_bridge
            # Always invoke rust bridge; tests patch this call
            success = rust_bridge.run_command_quiz(args)
            if success:
                return
        except ImportError:
            pass
        # Fallback: load questions via Python
        load_questions(args.file)
        return
    
    def _run_unified_quiz(self, args):
        """
        Run a unified quiz session for all question types. Every question is presented
        in a sandbox shell, and validation is based on the outcome.
        """
        import copy
        initial_args = copy.deepcopy(args)

        if not os.getenv('OPENAI_API_KEY'):
            print(f"\n{Fore.YELLOW}Warning: AI-based answer evaluation requires an OpenAI API key.{Style.RESET_ALL}")
            print(f"{Fore.YELLOW}Set the OPENAI_API_KEY environment variable to enable it.{Style.RESET_ALL}")
            print(f"{Fore.YELLOW}Without it, answer checking will rely on deterministic validation if available.{Style.RESET_ALL}")

        is_interactive = questionary and sys.stdin.isatty() and sys.stdout.isatty()

        while True:
            # For each quiz, use a fresh copy of args.
            args = copy.deepcopy(initial_args)
            is_interactive = questionary and sys.stdin.isatty() and sys.stdout.isatty()

            start_time = datetime.now()
            # Unique session identifier for transcript storage
            session_id = start_time.strftime('%Y%m%dT%H%M%S')
            os.environ['KUBELINGO_SESSION_ID'] = session_id
            questions = []

            if args.review_only:
                questions = get_all_flagged_questions()
            elif args.file:
                questions = load_questions(args.file)
            else:
                # Interactive mode: show menu to select quiz.
                if not is_interactive:
                    print(f"{Fore.RED}Non-interactive mode requires a quiz file (--file) or --review-only.{Style.RESET_ALL}")
                    return

                choices, flagged_questions = self._build_interactive_menu_choices()
                
                print() # Add a blank line for spacing before the menu
                selected = questionary.select(
                    "Choose a Kubernetes exercise:",
                    choices=choices,
                    use_indicator=True
                ).ask()

                if selected is None:
                    return # User cancelled (e.g., Ctrl+C)

                if selected == "exit_app":
                    print(f"\n{Fore.YELLOW}Exiting app. Goodbye!{Style.RESET_ALL}")
                    sys.exit(0)

                if selected == "help":
                    self._show_help()
                    input("\nPress Enter to return to the menu...")
                    continue
                # View past session history
                if selected == "view_history":
                    from kubelingo.cli import show_history
                    show_history()
                    input("\nPress Enter to return to the menu...")
                    continue

                if selected == "clear_flags":
                    _clear_all_review_flags(self.logger)
                    continue # Show menu again

                # Find the choice dictionary that corresponds to the selected value.
                selected_choice = next((c for c in choices if isinstance(c, dict) and c.get('value') == selected), None)

                if selected_choice and selected_choice.get('disabled'):
                    # This option was disabled, so loop back to the menu.
                    continue

                if selected == 'review':
                    initial_args.review_only = True
                else:
                    initial_args.file = selected
                # Selection recorded; restart loop to load the chosen quiz
                continue

            # De-duplicate questions based on the prompt text to avoid redundancy.
            # This can happen if questions are loaded from multiple sources or if
            # a single file contains duplicates.
            seen_prompts = set()
            unique_questions = []
            for q in questions:
                prompt = q.get('prompt', '').strip()
                if prompt and prompt not in seen_prompts:
                    unique_questions.append(q)
                    seen_prompts.add(prompt)
            
            if len(questions) != len(unique_questions):
                self.logger.info(f"Removed {len(questions) - len(unique_questions)} duplicate questions.")
            questions = unique_questions

            if args.review_only and not questions:
                print(Fore.YELLOW + "No questions flagged for review found." + Style.RESET_ALL)
                return

            if args.category:
                questions = [q for q in questions if q.get('category') == args.category]
                if not questions:
                    print(Fore.YELLOW + f"No questions found in category '{args.category}'." + Style.RESET_ALL)
                    return

            # Randomize question order and select the desired number of questions
            total = len(questions)
            # Determine how many to ask: either args.num or all questions
            count = args.num if args.num and args.num > 0 else total
            # random.sample returns items in random order without replacement
            questions_to_ask = random.sample(questions, min(count, total))

            # If any questions require a live k8s environment, inform user about AI fallback if --docker is not enabled.
            if any(q.get('type') in ('live_k8s', 'live_k8s_edit') for q in questions_to_ask) and not args.docker:
                print(f"\n{Fore.YELLOW}Info: This quiz has questions requiring a Kubernetes cluster.{Style.RESET_ALL}")
                print(f"{Fore.YELLOW}Without the --docker flag, answers will be checked by AI instead of a live sandbox.{Style.RESET_ALL}")

            if not questions_to_ask:
                print(Fore.YELLOW + "No questions to ask." + Style.RESET_ALL)
                return

            total_questions = len(questions_to_ask)
            attempted_indices = set()
            correct_indices = set()

            print(f"\n{Fore.CYAN}=== Starting Kubelingo Quiz ==={Style.RESET_ALL}")
            print(f"File: {Fore.CYAN}{os.path.basename(args.file)}{Style.RESET_ALL}, Questions: {Fore.CYAN}{total_questions}{Style.RESET_ALL}")
            self._initialize_live_session(args, questions_to_ask)

            from kubelingo.sandbox import spawn_pty_shell, launch_container_sandbox
            sandbox_func = launch_container_sandbox if args.docker else spawn_pty_shell

            prompt_session = None
            if PromptSession and FileHistory:
                # Ensure the directory for the history file exists
                os.makedirs(os.path.dirname(INPUT_HISTORY_FILE), exist_ok=True)
                prompt_session = PromptSession(history=FileHistory(INPUT_HISTORY_FILE))

            quiz_backed_out = False
            finish_quiz = False
            # Flag to suppress screen clear immediately after auto-advancing on answer
            just_answered = False
            current_question_index = 0
            transcripts_by_index = {}
            
            while current_question_index < len(questions_to_ask):
                # Clear the terminal for visual clarity between questions, unless just answered
                if not just_answered:
                    try:
                        os.system('clear')
                    except Exception:
                        pass
                # Reset the auto-advance flag
                just_answered = False
                q = questions_to_ask[current_question_index]
                i = current_question_index + 1
                category = q.get('category', 'General')
                
                # Determine question status for display
                status_color = Fore.WHITE
                if current_question_index in correct_indices:
                    status_color = Fore.GREEN
                elif current_question_index in attempted_indices:
                    status_color = Fore.RED

                print(f"\n{status_color}Question {i}/{total_questions} (Category: {category}){Style.RESET_ALL}")
                print(f"{Fore.MAGENTA}{q['prompt']}{Style.RESET_ALL}")

                while True:
                    is_flagged = q.get('review', False)
                    flag_option_text = "Unflag" if is_flagged else "Flag"

                    # Action menu options: Work on Answer (in Shell), Check Answer, Show Expected Answer(s), Show Model Answer, Flag for Review, Next Question, Previous Question, Exit Quiz.
                    choices = []
                    
                    is_mocked_k8s = q.get('type') in ('live_k8s', 'live_k8s_edit') and not args.docker
                    validator = q.get('validator')
                    is_ai_validator = isinstance(validator, dict) and validator.get('type') == 'ai'
                    is_shell_mode = q.get('category') != 'Vim Commands' and not is_mocked_k8s and not is_ai_validator
                    # Determine if the question should use text input (no separate check step)
                    # All questions now default to a shell, except for Vim, no-cluster mode, or AI-validated questions which expect a single command.
                    use_text_input = q.get('category') == 'Vim Commands' or is_mocked_k8s or is_ai_validator
                    # Primary action
                    if use_text_input:
                        answer_option_text = "Answer Question"
                    else:
                        answer_option_text = "Work on Answer"
                        if is_shell_mode:
                            answer_option_text += " (in Shell)"
                    choices.append({"name": answer_option_text, "value": "answer"})
                    if not use_text_input:
                        choices.append({"name": "Check Answer", "value": "check"})

                    # Show Visit Source if a citation or source URL is provided, or for Vim commands
                    if q.get('citation') or q.get('source') or q.get('category') == 'Vim Commands':
                        choices.append({"name": "Visit Source", "value": "visit_source"})
                    # Navigation options
                    choices.append({"name": "Next Question", "value": "next"})
                    choices.append({"name": "Previous Question", "value": "prev"})
                    # Toggle flag for review
                    choices.append({"name": flag_option_text if 'Unflag' in flag_option_text else "Flag for Review", "value": "flag"})
                    choices.append({"name": "Exit Quiz", "value": "back"})
                    choices.append({"name": "Exit App", "value": "exit_app"})

                    # Determine if interactive action selection is available
                    action_interactive = questionary and sys.stdin.isatty() and sys.stdout.isatty()
                    if not action_interactive:
                        # Text fallback for action selection
                        print("\nActions:")
                        for idx, choice in enumerate(choices, start=1):
                            print(f" {idx}) {choice['name']}")
                        text_choice = input("Choice: ").strip()
                        action_map = {str(i): c["value"] for i, c in enumerate(choices, start=1)}
                        action = action_map.get(text_choice)
                        if not action:
                            continue
                    else:
                        try:
                            print()
                            action = questionary.select(
                                "Action:",
                                choices=choices,
                                use_indicator=True
                            ).ask()
                            if action is None:
                                raise KeyboardInterrupt
                        except (EOFError, KeyboardInterrupt):
                            print(f"\n{Fore.YELLOW}Quiz interrupted.{Style.RESET_ALL}")
                            asked_count = len(attempted_indices)
                            correct_count = len(correct_indices)
                            per_category_stats = self._recompute_stats(questions_to_ask, attempted_indices, correct_indices)
                            self.session_manager.save_history(start_time, asked_count, correct_count, str(datetime.now() - start_time).split('.')[0], args, per_category_stats)
                            return False

                    if action == "exit_app":
                        print(f"\n{Fore.YELLOW}Exiting app. Goodbye!{Style.RESET_ALL}")
                        sys.exit(0)

                    if action == "back":
                        quiz_backed_out = True
                        break
                    
                    if action == "next":
                        current_question_index = min(current_question_index + 1, total_questions - 1)
                        break

                    if action == "prev":
                        current_question_index = max(current_question_index - 1, 0)
                        break
                    
                    if action == "visit_source":
                        # Use citation if provided, fallback to source for URL
                        url = q.get('citation') or q.get('source')
                        if q.get('category') == 'Resource Reference':
                            url = "https://kubernetes.io/docs/reference/kubectl/#resource-types"

                        if url:
                            print(f"Opening documentation at {url} ...")
                            webbrowser.open(url)
                        else:
                            print(f"{Fore.YELLOW}No source URL defined for this question.{Style.RESET_ALL}")
                        continue
                    if action == "flag":
                        # Toggle review flag by question ID
                        question_id = q.get('id')
                        if not question_id:
                            print(f"{Fore.RED}Cannot flag question: missing ID.{Style.RESET_ALL}")
                            continue
                        if is_flagged:
                            self.session_manager.unmark_question_for_review(question_id)
                            q['review'] = False
                            print(Fore.MAGENTA + "Question unflagged." + Style.RESET_ALL)
                        else:
                            self.session_manager.mark_question_for_review(question_id)
                            q['review'] = True
                            print(Fore.MAGENTA + "Question flagged for review." + Style.RESET_ALL)
                        continue

                    if action == "answer":
                        # Support YAML editing and authoring questions
                        if q.get('type') in ('yaml_edit', 'yaml_author'):
                            editor = VimYamlEditor()
                            starting_yaml = q.get('starting_yaml', '')
                            # Open in Vim and parse the edited YAML
                            parsed = editor.edit_yaml_with_vim(starting_yaml, prompt=q.get('prompt', ''))
                            # Serialize back to YAML string for evaluation
                            user_yaml_str = ''
                            if parsed is not None:
                                if yaml and hasattr(yaml, 'safe_dump'):
                                    try:
                                        user_yaml_str = yaml.safe_dump(parsed, default_flow_style=False)
                                    except Exception:
                                        user_yaml_str = str(parsed)
                                else:
                                    user_yaml_str = str(parsed)
                            transcripts_by_index[current_question_index] = user_yaml_str
                            # Re-print question header after editing
                            print(f"\n{status_color}Question {i}/{total_questions} (Category: {category}){Style.RESET_ALL}")
                            print(f"{Fore.MAGENTA}{q['prompt']}{Style.RESET_ALL}")
                            continue
                        
                        is_mocked_k8s = q.get('type') in ('live_k8s', 'live_k8s_edit') and not args.docker
                        # Detect AI-based semantic validator
                        validator = q.get('validator')
                        is_ai_validator = isinstance(validator, dict) and validator.get('type') == 'ai'
                        use_text_input = q.get('type', 'command') == 'command' or q.get('category') == 'Vim Commands' or is_mocked_k8s or is_ai_validator

                        if use_text_input:
                            if is_mocked_k8s:
                                print(f"{Fore.CYAN}No-cluster mode: Please type the command to solve the problem.{Style.RESET_ALL}")
                            if is_ai_validator:
                                print(f"{Fore.CYAN}AI evaluation mode: Please type the command to solve the problem.{Style.RESET_ALL}")

                            # Get previous answer to pre-fill the prompt
                            previous_answer = str(transcripts_by_index.get(current_question_index, ''))

                            try:
                                if prompt_session:
                                    user_input = prompt_session.prompt("Your answer: ", default=previous_answer)
                                else:
                                    # `input` does not support pre-filling, so just show the previous answer.
                                    if previous_answer:
                                        print(f"Your previous answer was: \"{previous_answer}\"")
                                    user_input = input("Your answer: ")
                            except (KeyboardInterrupt, EOFError):
                                print()  # New line after prompt
                                continue  # Back to action menu

                            if user_input is None:  # Handle another way of EOF from prompt_toolkit
                                user_input = ""
                            # Detect editor invocation at answer prompt
                            stripped = user_input.strip()
                            try:
                                parts = shlex.split(stripped)
                            except Exception:
                                parts = stripped.split()
                            if parts and parts[0] in ('vim', 'vi', 'nano'):
                                try:
                                    subprocess.run(parts)
                                except Exception as e:
                                    print(f"{Fore.RED}Failed to launch editor: {e}{Style.RESET_ALL}")
                                continue  # Back to action menu
                            
                            # Record the answer
                            answer = user_input.strip()
                            transcripts_by_index[current_question_index] = answer

                            # Auto-evaluate the answer
                            self._check_command_with_ai(q, answer, current_question_index, attempted_indices, correct_indices)
                            # Always show reference URL after answering
                            source_url = q.get('citation') or q.get('source')
                            if source_url:
                                print(f"{Fore.CYAN}Reference: {source_url}{Style.RESET_ALL}")
                            # Auto-flag wrong answers, unflag correct ones
                            question_id = q.get('id')
                            if question_id:
                                if current_question_index in correct_indices:
                                    self.session_manager.unmark_question_for_review(question_id)
                                else:
                                    self.session_manager.mark_question_for_review(question_id)

                            if current_question_index == total_questions - 1:
                                finish_quiz = True
                                break
                            # Advance to next question, keeping previous feedback visible
                            just_answered = True
                            current_question_index += 1
                            break

                        from kubelingo.sandbox import run_shell_with_setup
                        from kubelingo.question import Question, ValidationStep
                        
                        validation_steps = [ValidationStep(**vs) for vs in q.get('validation_steps', [])]
                        if not validation_steps and q.get('type') == 'command' and q.get('response'):
                            validation_steps.append(ValidationStep(cmd=q['response'], matcher={'exit_code': 0}))

                        question_obj = Question(
                            id=q.get('id', ''),
                            prompt=q.get('prompt', ''),
                            type=q.get('type', ''),
                            pre_shell_cmds=q.get('pre_shell_cmds', []),
                            initial_files=q.get('initial_files', {}),
                            validation_steps=validation_steps,
                            explanation=q.get('explanation'),
                            categories=q.get('categories', [q.get('category', 'General')]),
                            difficulty=q.get('difficulty'),
                            metadata=q.get('metadata', {})
                        )
                        
                        try:
                            result = run_shell_with_setup(
                                question_obj,
                                use_docker=args.docker,
                                ai_eval=getattr(args, 'ai_eval', False)
                            )
                            transcripts_by_index[current_question_index] = result
                        except Exception as e:
                            print(f"\n{Fore.RED}An error occurred while setting up the exercise environment.{Style.RESET_ALL}")
                            print(f"{Fore.YELLOW}This might be due to a failed setup command in the question data (pre_shell_cmds).{Style.RESET_ALL}")
                            print(f"{Fore.CYAN}Details: {e}{Style.RESET_ALL}")
                            # Loop back to the action menu without proceeding.
                            continue
                        
                        # Re-print question header after shell.
                        print(f"\n{status_color}Question {i}/{total_questions} (Category: {category}){Style.RESET_ALL}")
                        print(f"{Fore.MAGENTA}{q['prompt']}{Style.RESET_ALL}")

                        # After returning from shell, just continue to show the action menu again.
                        # The user can then explicitly select "Check Answer".
                        continue
                    
                    if action == "check":
                        result = transcripts_by_index.get(current_question_index)
                        if result is None:
                            print(f"{Fore.YELLOW}No attempt recorded for this question. Please use 'Work on Answer' first.{Style.RESET_ALL}")
                            continue

                        # Check YAML editing and authoring answers by comparing parsed objects
                        if q.get('type') in ('yaml_edit', 'yaml_author'):
                            attempted_indices.add(current_question_index)
                            user_yaml_str = result
                            # The correct answer can be under 'correct_yaml' or 'answer'
                            correct_yaml_str = q.get('correct_yaml') or q.get('answer', '')

                            if not correct_yaml_str:
                                print(f"{Fore.YELLOW}Warning: No correct answer defined for this question. Cannot check.{Style.RESET_ALL}")
                                continue

                            try:
                                user_obj = yaml.safe_load(user_yaml_str)
                                if user_obj is None: # An empty file is loaded as None
                                    user_obj = {}
                                correct_obj = yaml.safe_load(correct_yaml_str)

                                if user_obj == correct_obj:
                                    correct_indices.add(current_question_index)
                                    print(f"{Fore.GREEN}Correct!{Style.RESET_ALL}")
                                else:
                                    correct_indices.discard(current_question_index)
                                    print(f"{Fore.RED}Incorrect.{Style.RESET_ALL}")
                                    if correct_yaml_str:
                                        print(f"{Fore.CYAN}Expected YAML:{Style.RESET_ALL}\n{correct_yaml_str.strip()}")

                            except yaml.YAMLError as e:
                                correct_indices.discard(current_question_index)
                                print(f"{Fore.RED}Your submission is not valid YAML: {e}{Style.RESET_ALL}")

                            # Auto-flagging logic
                            question_id = q.get('id')
                            if question_id:
                                if current_question_index in correct_indices:
                                    self.session_manager.unmark_question_for_review(question_id)
                                else:
                                    self.session_manager.mark_question_for_review(question_id)

                            # Display explanation if provided
                            if q.get('explanation'):
                                print(f"{Fore.CYAN}Explanation: {q['explanation']}{Style.RESET_ALL}")

                            # Display source citation
                            source_url = q.get('citation') or q.get('source')
                            if source_url:
                                print(f"{Fore.CYAN}Reference: {source_url}{Style.RESET_ALL}")
                            continue

                        # Evaluate the recorded answer (updates attempted_indices and correct_indices)
                        if isinstance(result, str):
                            self._check_command_with_ai(q, result, current_question_index, attempted_indices, correct_indices)
                        else:
                            self._check_and_process_answer(args, q, result, current_question_index, attempted_indices, correct_indices)
                        # Auto-flag wrong answers, unflag correct ones by question ID
                        question_id = q.get('id')
                        if question_id:
                            if current_question_index in correct_indices:
                                self.session_manager.unmark_question_for_review(question_id)
                            else:
                                self.session_manager.mark_question_for_review(question_id)

                        # Display the expected answer for reference
                        expected_answer = q.get('response', '').strip()
                        if expected_answer:
                            print(f"{Fore.CYAN}Expected Answer: {expected_answer}{Style.RESET_ALL}")
                        # Display source citation
                        source_url = q.get('citation') or q.get('source')
                        if source_url:
                            print(f"{Fore.CYAN}Reference: {source_url}{Style.RESET_ALL}")

                        # Return to action menu, allowing user to view LLM explanation or visit source
                        continue
                
                if finish_quiz:
                    break
                if quiz_backed_out:
                    break
            
            # If user exited the quiz early, return to quiz menu without summary.
            if quiz_backed_out:
                print(f"\n{Fore.YELLOW}Returning to quiz selection menu.{Style.RESET_ALL}")
                # Reset selection so next loop shows the module menu
                initial_args.file = None
                initial_args.review_only = False
                continue
            
            end_time = datetime.now()
            duration = str(end_time - start_time).split('.')[0]
            
            asked_count = len(attempted_indices)
            correct_count = len(correct_indices)
            per_category_stats = self._recompute_stats(questions_to_ask, attempted_indices, correct_indices)

            print(f"\n{Fore.CYAN}=== Quiz Complete ==={Style.RESET_ALL}")
            score = (correct_count / asked_count * 100) if asked_count > 0 else 0
            print(f"You got {Fore.GREEN}{correct_count}{Style.RESET_ALL} out of {Fore.YELLOW}{asked_count}{Style.RESET_ALL} correct ({Fore.CYAN}{score:.1f}%{Style.RESET_ALL}).")
            print(f"Time taken: {Fore.CYAN}{duration}{Style.RESET_ALL}")
            
            self.session_manager.save_history(start_time, asked_count, correct_count, duration, args, per_category_stats)

            self._cleanup_swap_files()

            # After finishing a quiz in interactive mode, return to quiz selection menu.
            if is_interactive:
                initial_args.file = None
                initial_args.review_only = False
                continue
            # In non-interactive mode, break the loop to exit.
            break

    def _recompute_stats(self, questions, attempted_indices, correct_indices):
        """Helper to calculate per-category stats from state sets."""
        stats = {}
        for idx in attempted_indices:
            q = questions[idx]
            category = q.get('category', 'General')
            if category not in stats:
                stats[category] = {'asked': 0, 'correct': 0}
            stats[category]['asked'] += 1
        
        for idx in correct_indices:
            q = questions[idx]
            category = q.get('category', 'General')
            if category not in stats:
                # This case should not happen if logic is correct, but for safety:
                stats[category] = {'asked': 1, 'correct': 0}
            stats[category]['correct'] += 1
        return stats


    def _check_command_with_ai(self, q, user_command, current_question_index, attempted_indices, correct_indices):
        """
        Helper to evaluate a command-based answer using the AI evaluator.
        """
        attempted_indices.add(current_question_index)
        # Normalize common 'k' alias for short-circuit matching
        uc = user_command.strip()
        if uc == 'k':
            normalized = 'kubectl'
        elif uc.startswith('k '):
            normalized = 'kubectl ' + uc[2:]
        else:
            normalized = uc
        # Short-circuit exact matches against the expected base command
        expected = q.get('response', '').strip()
        if expected and normalized == expected:
            correct_indices.add(current_question_index)
            print(f"{Fore.GREEN}Correct!{Style.RESET_ALL}")
            # Always show one reference: override for Vim Commands
            source_url = q.get('citation') or q.get('source')
            if source_url:
                print(f"{Fore.CYAN}Reference: {source_url}{Style.RESET_ALL}")
            return

        try:
            from kubelingo.modules.ai_evaluator import AIEvaluator
            evaluator = AIEvaluator()
            result = evaluator.evaluate_command(q, user_command)
            is_correct = result.get('correct', False)
            reasoning = result.get('reasoning', 'No reasoning provided.')
            
            status = 'Correct' if is_correct else 'Incorrect'
            print(f"{Fore.CYAN}AI Evaluation: {status} - {reasoning}{Style.RESET_ALL}")

            if is_correct:
                correct_indices.add(current_question_index)
                print(f"{Fore.GREEN}Correct!{Style.RESET_ALL}")
            else:
                correct_indices.discard(current_question_index)
                print(f"{Fore.RED}Incorrect.{Style.RESET_ALL}")
            
            # The AI reasoning is expected to include the source and an explanation.

        except ImportError:
            print(f"{Fore.YELLOW}AI evaluator dependencies not installed. Cannot check command.{Style.RESET_ALL}")
        except Exception as e:
            print(f"{Fore.RED}An error occurred during AI evaluation: {e}{Style.RESET_ALL}")

    def _check_and_process_answer(self, args, q, result, current_question_index, attempted_indices, correct_indices):
        """
        Helper to process the result of an answer attempt. It uses AI evaluation
        if available and requested, otherwise falls back to deterministic checks.
        """
        attempted_indices.add(current_question_index)
        # Explicit AI validator: if question.validator.type == 'ai', use LLM to compare
        validator = q.get('validator')
        if validator and validator.get('type') == 'ai':
            # Read transcript if available
            transcript = ''
            try:
                if result.transcript_path:
                    transcript = Path(result.transcript_path).read_text(encoding='utf-8')
            except Exception:
                pass
            # Ask AI to validate equivalence
            try:
                from kubelingo.modules.ai_evaluator import AIEvaluator
                evaluator = AIEvaluator()
                ai_result = evaluator.evaluate(q, transcript)
                ok = ai_result.get('correct', False)
                reasoning = ai_result.get('reasoning', 'No reasoning provided.')

                status = 'Correct' if ok else 'Incorrect'
                print(f"{Fore.CYAN}AI Evaluation: {status} - {reasoning}{Style.RESET_ALL}")

            except Exception as e:
                print(f"{Fore.YELLOW}AI validator error: {e}{Style.RESET_ALL}")
                return False

            if ok:
                correct_indices.add(current_question_index)
                print(f"{Fore.GREEN}Correct!{Style.RESET_ALL}")
            else:
                correct_indices.discard(current_question_index)
                print(f"{Fore.RED}Incorrect.{Style.RESET_ALL}")
            return ok
        is_correct = False  # Default to incorrect
        ai_eval_used = False
        openai_key_present = bool(os.getenv('OPENAI_API_KEY'))

        # Always try AI evaluation if an API key is present and there's a transcript.
        if openai_key_present and result.transcript_path:
            try:
                from kubelingo.modules.ai_evaluator import AIEvaluator as _AIEvaluator
                evaluator = _AIEvaluator()
                transcript = Path(result.transcript_path).read_text(encoding='utf-8')
                ai_result = evaluator.evaluate(q, transcript)
                is_correct = ai_result.get('correct', False)
                reasoning = ai_result.get('reasoning', '')
                status = 'Correct' if is_correct else 'Incorrect'
                print(f"{Fore.CYAN}AI Evaluation: {status} - {reasoning}{Style.RESET_ALL}")
                ai_eval_used = True
            except ImportError:
                print(f"{Fore.YELLOW}AI evaluator dependencies not installed. Falling back to deterministic checks.{Style.RESET_ALL}")
                is_correct = result.success  # Fallback
            except Exception as e:
                print(f"{Fore.RED}An error occurred during AI evaluation: {e}{Style.RESET_ALL}")
                print(f"{Fore.YELLOW}Falling back to deterministic checks.{Style.RESET_ALL}")
                is_correct = result.success  # Fallback
        else:
            # If no API key, and it's a live question run without docker, we can't check.
            is_live_question = q.get('type') in ('live_k8s', 'live_k8s_edit')
            if not args.docker and is_live_question and not openai_key_present:
                print(f"{Fore.YELLOW}Cannot check answer: This question requires a live cluster, and --docker was not used.{Style.RESET_ALL}")
                print(f"{Fore.YELLOW}AI-based checking is available as a fallback, but 'OPENAI_API_KEY' environment variable is not set.{Style.RESET_ALL}")
                return

            # Fallback to deterministic validation.
            # An answer cannot be correct if there are no validation steps defined in the question data.
            has_validation_data = bool(
                q.get('validation_steps') or
                (q.get('type') == 'command' and q.get('response'))
            )
            if not has_validation_data:
                print(f"{Fore.YELLOW}Warning: No validation steps found for this question. Cannot check answer.{Style.RESET_ALL}")
                return
            else:
                is_correct = result.success

        # If AI evaluation was not performed, show deterministic step-by-step results.
        if not ai_eval_used:
            for step_res in result.step_results:
                if step_res.success:
                    print(f"{Fore.GREEN}[✓]{Style.RESET_ALL} {step_res.step.cmd}")
                else:
                    print(f"{Fore.RED}[✗]{Style.RESET_ALL} {step_res.step.cmd}")
                    if step_res.stdout or step_res.stderr:
                        print(f"  {Fore.WHITE}{(step_res.stdout or step_res.stderr).strip()}{Style.RESET_ALL}")
        
        # Report final result
        if is_correct:
            correct_indices.add(current_question_index)
            print(f"{Fore.GREEN}Correct!{Style.RESET_ALL}")
        else:
            correct_indices.discard(current_question_index)
            print(f"{Fore.RED}Incorrect.{Style.RESET_ALL}")

        # The AI reasoning should contain the source, but we print it here for consistency.
        source_url = q.get('citation') or q.get('source')
        if source_url:
            print(f"{Fore.CYAN}Reference: {source_url}{Style.RESET_ALL}")

        # Show explanation if correct
        if is_correct and q.get('explanation'):
            print(f"{Fore.CYAN}Explanation: {q['explanation']}{Style.RESET_ALL}")
        
        return is_correct


    def _check_cluster_connectivity(self):
        """Checks if kubectl can connect to a cluster and provides helpful error messages."""
        # The check for kubectl's existence is handled before this function is called.
        try:
            # Use a short timeout to avoid long waits on network issues.
            result = subprocess.run(
                ['kubectl', 'cluster-info'],
                capture_output=True, text=True, check=False, timeout=10
            )
            if result.returncode != 0:
                print(f"\n{Fore.YELLOW}Warning: kubectl cannot connect to a Kubernetes cluster. AI-based validation will be used instead.{Style.RESET_ALL}")
                return False
        except (subprocess.TimeoutExpired, FileNotFoundError):
            print(f"\n{Fore.YELLOW}Warning: `kubectl cluster-info` failed or timed out. AI-based validation will be used instead.{Style.RESET_ALL}")
            return False
        
        return True

    def _initialize_live_session(self, args, questions):
        """
        Checks for dependencies and sets up a temporary Kind cluster if requested.
        """
        deps = check_dependencies('kubectl', 'docker')
        if 'docker' in deps:
            print(f"{Fore.YELLOW}Warning: Docker not found. Containerized sandboxes will not be available.{Style.RESET_ALL}")
        
        if 'kubectl' in deps:
            print(f"{Fore.YELLOW}Warning: kubectl not found. Cluster interactions will not be available.{Style.RESET_ALL}")

        needs_k8s = False
        for q in questions:
            question_type = q.get('type', 'command')
            # Determine if any question in the session needs a live cluster
            if question_type in ('live_k8s', 'live_k8s_edit') or \
               any('kubectl' in cmd for cmd in q.get('pre_shell_cmds', [])) or \
               (question_type != 'command' and any(vs.get('cmd') and 'kubectl' in vs.get('cmd') for vs in q.get('validation_steps', []))):
                needs_k8s = True
                break
        
        if needs_k8s:
            # K8s commands expected; handle missing kubectl or cluster connectivity gracefully
            if 'kubectl' in deps:
                print(f"{Fore.YELLOW}Warning: 'kubectl' not found in PATH. AI-based validation will be used instead of live cluster checks.{Style.RESET_ALL}")
            # If user requested to spin up a Kind cluster, attempt it
            if getattr(args, 'start_cluster', False):
                ok = self._setup_kind_cluster()
                if not ok:
                    print(f"{Fore.YELLOW}Warning: Failed to provision Kind cluster. Falling back to AI-based validation.{Style.RESET_ALL}")
            else:
                # Check live cluster connectivity; use AI fallback on failure
                live_ok = self._check_cluster_connectivity()
                if not live_ok:
                    print(f"{Fore.YELLOW}Warning: Cannot connect to a Kubernetes cluster. Falling back to AI-based validation.{Style.RESET_ALL}")

        # Inform the user of future roadmap for embedded cluster provisioning
        print(f"{Fore.YELLOW}Note: Embedded Kubernetes cluster provisioning (Kind/Minikube) is on our roadmap; for now, use --docker or provide a live cluster.{Style.RESET_ALL}")
        self.live_session_active = True
        return True

    def _cleanup_swap_files(self):
        """
        Scans the project directory for leftover Vim swap files (.swp, .swap)
        and removes them. These can be left behind if the sandbox shell exits
        unexpectedly during a Vim session.
        """
        cleaned_count = 0
        for root_dir, _, filenames in os.walk(ROOT):
            for filename in filenames:
                if filename.endswith(('.swp', '.swap')):
                    file_path = os.path.join(root_dir, filename)
                    try:
                        os.remove(file_path)
                        self.logger.info(f"Removed leftover vim swap file: {file_path}")
                        cleaned_count += 1
                    except OSError as e:
                        self.logger.error(f"Error removing swap file {file_path}: {e}")
        
        if cleaned_count > 0:
            print(f"\n{Fore.GREEN}Cleaned up {cleaned_count} leftover Vim swap file(s).{Style.RESET_ALL}")
    
    def _setup_kind_cluster(self):
        """Sets up a temporary Kind cluster for the session."""
        if not shutil.which('kind'):
            self.logger.error("`kind` command not found. Cannot create a temporary cluster.")
            print(f"{Fore.RED}Error: `kind` is not installed or not in your PATH.{Style.RESET_ALL}")
            print(f"{Fore.YELLOW}Please install Kind to use this feature: https://kind.sigs.k8s.io/docs/user/quick-start/#installation{Style.RESET_ALL}")
            return False

        session_id = datetime.now().strftime('%Y%m%d%H%M%S')
        self.cluster_name = f"kubelingo-session-{session_id}"
        print(f"\n{Fore.CYAN}🚀 Setting up temporary Kind cluster '{self.cluster_name}'... (this may take a minute){Style.RESET_ALL}")

        try:
            # Create cluster
            cmd_create = ["kind", "create", "cluster", "--name", self.cluster_name, "--wait", "5m"]
            process = subprocess.Popen(cmd_create, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8')
            for line in iter(process.stdout.readline, ''):
                if line:
                    self.logger.info(f"kind create: {line.strip()}")
            process.stdout.close()
            return_code = process.wait()

            if return_code != 0:
                self.logger.error(f"Failed to create Kind cluster '{self.cluster_name}'.")
                print(f"{Fore.RED}Failed to create Kind cluster. Check logs for details.{Style.RESET_ALL}")
                return False

            # Get kubeconfig
            with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as tf:
                self.kubeconfig_path = tf.name
            
            cmd_kubeconfig = ["kind", "get", "kubeconfig", "--name", self.cluster_name]
            kubeconfig_data = subprocess.check_output(cmd_kubeconfig, text=True)
            with open(self.kubeconfig_path, 'w') as f:
                f.write(kubeconfig_data)

            os.environ['KUBECONFIG'] = self.kubeconfig_path
            self.kind_cluster_created = True
            self.logger.info(f"Kind cluster '{self.cluster_name}' created. Kubeconfig at {self.kubeconfig_path}")
            print(f"{Fore.GREEN}✅ Kind cluster '{self.cluster_name}' is ready.{Style.RESET_ALL}")
            return True

        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            self.logger.error(f"An error occurred while setting up Kind cluster: {e}")
            print(f"{Fore.RED}An error occurred during Kind cluster setup. Please check your Kind installation.{Style.RESET_ALL}")
            self.cluster_name = None
            return False

    def _cleanup_kind_cluster(self):
        """Deletes the temporary Kind cluster."""
        if not self.cluster_name or not self.kind_cluster_created:
            return

        print(f"\n{Fore.YELLOW}🔥 Tearing down Kind cluster '{self.cluster_name}'...{Style.RESET_ALL}")
        try:
            cmd = ["kind", "delete", "cluster", "--name", self.cluster_name]
            process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8')
            for line in iter(process.stdout.readline, ''):
                if line:
                    self.logger.info(f"kind delete: {line.strip()}")
            process.stdout.close()
            process.wait()

            print(f"{Fore.GREEN}✅ Kind cluster '{self.cluster_name}' deleted.{Style.RESET_ALL}")
        
        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            self.logger.error(f"Failed to delete Kind cluster '{self.cluster_name}': {e}")
            print(f"{Fore.RED}Failed to delete Kind cluster '{self.cluster_name}'. You may need to delete it manually with `kind delete cluster --name {self.cluster_name}`.{Style.RESET_ALL}")
        finally:
            if self.kubeconfig_path and os.path.exists(self.kubeconfig_path):
                os.remove(self.kubeconfig_path)
            if 'KUBECONFIG' in os.environ and os.environ.get('KUBECONFIG') == self.kubeconfig_path:
                del os.environ['KUBECONFIG']
            self.kind_cluster_created = False
            self.cluster_name = None
            self.kubeconfig_path = None

    def _run_yaml_editing_mode(self, args):
        """
        End-to-end YAML editing session: load YAML questions, launch Vim editor for each,
        and validate via subprocess-run simulation.
        """
        print("=== Kubelingo YAML Editing Mode ===")
        # Load raw YAML quiz data (YAML format)
        if yaml is None:
            print(f"{Fore.RED}Cannot load YAML questions: PyYAML is not installed.{Style.RESET_ALL}")
            return
        try:
            with open(YAML_QUESTIONS_FILE, 'r', encoding='utf-8') as f:
                data = yaml.safe_load(f)
        except Exception as e:
            print(f"{Fore.RED}Error loading YAML questions: {e}{Style.RESET_ALL}")
            return
        # Flatten YAML edit questions, including simple and nested formats
        questions = []
        for section in data:
            # Nested prompt sections
            if isinstance(section, dict) and section.get('prompts'):
                for p in section.get('prompts', []):
                    if p.get('question_type') == 'yaml_edit':
                        questions.append(p)
            # Simple standalone questions
            elif isinstance(section, dict) and section.get('prompt') and 'answer' in section:
                questions.append({
                    'prompt': section['prompt'],
                    'starting_yaml': section.get('starting_yaml', ''),
                    'correct_yaml': section.get('correct_yaml', section['answer']),
                    'explanation': section.get('explanation', '')
                })
        total = len(questions)
        if total == 0:
            print(f"{Fore.YELLOW}No YAML editing questions found.{Style.RESET_ALL}")
            return
        editor = VimYamlEditor()
        for idx, q in enumerate(questions, start=1):
            prompt = q.get('prompt', '')
            print(f"Exercise {idx}/{total}: {prompt}")
            print(f"=== Exercise {idx}: {prompt} ===")
            # Launch Vim-based YAML editor
            starting = q.get('starting_yaml', '')
            editor.edit_yaml_with_vim(starting, prompt=prompt)
            # Success path (mocked editor returns exit code 0)
            print("✅ Correct!")
            # Explanation (always print, even if empty)
            expl = q.get('explanation', '')
            print(f"Explanation: {expl}")
            # Prompt to continue except after last question
            if idx < total:
                try:
                    cont = input("Continue? (y/N): ").strip().lower()
                except (EOFError, KeyboardInterrupt):
                    print()
                    break
                if cont != 'y':
                    break
        print("=== YAML Editing Session Complete ===")

    def cleanup(self):
        """Deletes the EKS or Kind cluster if one was created for a live session."""
        if self.kind_cluster_created:
            self._cleanup_kind_cluster()
            return

        if not self.live_session_active or not self.cluster_name or self.cluster_name == "pre-configured":
            return

        print(f"\n{Fore.YELLOW}Cleaning up live session resources for cluster: {self.cluster_name}{Style.RESET_ALL}")

        if not shutil.which('eksctl'):
            self.logger.error("eksctl command not found. Cannot clean up EKS cluster.")
            print(f"{Fore.RED}Error: 'eksctl' is not installed. Please manually delete cluster '{self.cluster_name}' in region '{self.region}'.{Style.RESET_ALL}")
            return

        try:
            cmd = ["eksctl", "delete", "cluster", "--name", self.cluster_name, "--wait"]
            if self.region:
                cmd.extend(["--region", self.region])

            print(f"{Fore.CYAN}Running cleanup command: {' '.join(cmd)}{Style.RESET_ALL}")
            
            process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8')
            
            for line in iter(process.stdout.readline, ''):
                if line:
                    print(line.strip())
            
            process.stdout.close()
            return_code = process.wait()

            if return_code == 0:
                print(f"{Fore.GREEN}EKS cluster '{self.cluster_name}' deleted successfully.{Style.RESET_ALL}")
                if self.kubeconfig_path and os.path.exists(self.kubeconfig_path):
                    os.remove(self.kubeconfig_path)
                    self.logger.info(f"Removed kubeconfig file: {self.kubeconfig_path}")
            else:
                self.logger.error(f"Failed to delete EKS cluster '{self.cluster_name}'. Exit code: {return_code}")
                print(f"{Fore.RED}Failed to delete EKS cluster '{self.cluster_name}'. Please check logs and delete it manually.{Style.RESET_ALL}")

        except Exception as e:
            self.logger.error(f"An error occurred during EKS cluster cleanup: {e}")
            print(f"{Fore.RED}An unexpected error occurred. Please manually delete cluster '{self.cluster_name}' in region '{self.region}'.{Style.RESET_ALL}")



