"""Security Training Generator v2 for MCP environments.

Generates personalized security training material from real scan findings.
Supports multiple training modules: awareness, secure coding, MCP security,
OWASP, and tool safety. Includes quiz generator with multiple-choice
questions and curriculum planner.

CLI: mcpaudit training <path> [--level beginner|intermediate|advanced]
     mcpaudit training <path> --quiz --num-questions 10
     mcpaudit training <path> --curriculum

Zero external dependencies. Python 3.11+ stdlib only.
"""

from __future__ import annotations

import hashlib
import html as html_mod
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any


__all__ = [
    "Answer",
    "Curriculum",
    "CurriculumModule",
    "Question",
    "Quiz",
    "TrainingLevel",
    "TrainingModule",
    "TrainingModuleType",
    "TrainingMaterial",
    "TrainingSection",
    "format_curriculum_json",
    "format_curriculum_text",
    "format_quiz_json",
    "format_quiz_text",
    "format_training_html",
    "format_training_json",
    "format_training_text",
    "generate_curriculum",
    "generate_quiz",
    "generate_training",
    "get_module_types",
]


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

class TrainingLevel(Enum):
    """Training difficulty levels."""

    BEGINNER = "beginner"
    INTERMEDIATE = "intermediate"
    ADVANCED = "advanced"

    @classmethod
    def from_string(cls, value: str) -> TrainingLevel:
        normalized = value.lower().strip()
        for member in cls:
            if member.value == normalized:
                return member
        valid = ", ".join(m.value for m in cls)
        raise ValueError(f"Unknown training level '{value}'. Valid: {valid}")

    @property
    def label(self) -> str:
        return self.value.title()

    @property
    def depth(self) -> int:
        """Depth indicator: 1=beginner, 2=intermediate, 3=advanced."""
        return {
            TrainingLevel.BEGINNER: 1,
            TrainingLevel.INTERMEDIATE: 2,
            TrainingLevel.ADVANCED: 3,
        }[self]


class TrainingModuleType(Enum):
    """Types of training modules."""

    AWARENESS = "awareness"
    SECURE_CODING = "secure_coding"
    MCP_SECURITY = "mcp_security"
    OWASP = "owasp"
    TOOL_SAFETY = "tool_safety"

    @classmethod
    def from_string(cls, value: str) -> TrainingModuleType:
        normalized = value.lower().strip().replace("-", "_").replace(" ", "_")
        for member in cls:
            if member.value == normalized:
                return member
        valid = ", ".join(m.value for m in cls)
        raise ValueError(f"Unknown module type '{value}'. Valid: {valid}")

    @property
    def label(self) -> str:
        return {
            TrainingModuleType.AWARENESS: "Security Awareness",
            TrainingModuleType.SECURE_CODING: "Secure Coding Practices",
            TrainingModuleType.MCP_SECURITY: "MCP Security Fundamentals",
            TrainingModuleType.OWASP: "OWASP Top 10 for MCP",
            TrainingModuleType.TOOL_SAFETY: "Tool Safety & Access Control",
        }[self]

    @property
    def estimated_minutes(self) -> int:
        """Base estimated duration in minutes."""
        return {
            TrainingModuleType.AWARENESS: 30,
            TrainingModuleType.SECURE_CODING: 60,
            TrainingModuleType.MCP_SECURITY: 45,
            TrainingModuleType.OWASP: 60,
            TrainingModuleType.TOOL_SAFETY: 30,
        }[self]


# ---------------------------------------------------------------------------
# Data Models
# ---------------------------------------------------------------------------

@dataclass
class TrainingSection:
    """A section within a training module."""

    title: str
    content: str
    code_example: str = ""
    bad_example: str = ""
    good_example: str = ""
    key_points: list[str] = field(default_factory=list)
    references: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "content": self.content,
            "code_example": self.code_example,
            "bad_example": self.bad_example,
            "good_example": self.good_example,
            "key_points": self.key_points,
            "references": self.references,
        }


@dataclass
class TrainingModule:
    """A complete training module with sections."""

    module_type: TrainingModuleType
    title: str
    description: str
    sections: list[TrainingSection] = field(default_factory=list)
    level: TrainingLevel = TrainingLevel.BEGINNER
    estimated_minutes: int = 30
    prerequisites: list[str] = field(default_factory=list)
    objectives: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "module_type": self.module_type.value,
            "title": self.title,
            "description": self.description,
            "sections": [s.to_dict() for s in self.sections],
            "level": self.level.value,
            "estimated_minutes": self.estimated_minutes,
            "prerequisites": self.prerequisites,
            "objectives": self.objectives,
            "total_sections": len(self.sections),
        }


@dataclass
class Answer:
    """A quiz answer option."""

    text: str
    is_correct: bool = False
    explanation: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "text": self.text,
            "is_correct": self.is_correct,
            "explanation": self.explanation,
        }


@dataclass
class Question:
    """A quiz question with multiple-choice answers."""

    question_id: str
    text: str
    answers: list[Answer] = field(default_factory=list)
    difficulty: TrainingLevel = TrainingLevel.BEGINNER
    category: str = ""
    related_rule_id: str = ""
    explanation: str = ""

    @property
    def correct_answer(self) -> Answer | None:
        for a in self.answers:
            if a.is_correct:
                return a
        return None

    def to_dict(self) -> dict[str, Any]:
        return {
            "question_id": self.question_id,
            "text": self.text,
            "answers": [a.to_dict() for a in self.answers],
            "difficulty": self.difficulty.value,
            "category": self.category,
            "related_rule_id": self.related_rule_id,
            "explanation": self.explanation,
        }


@dataclass
class Quiz:
    """A quiz with multiple questions."""

    title: str
    questions: list[Question] = field(default_factory=list)
    level: TrainingLevel = TrainingLevel.BEGINNER
    passing_score: float = 0.7
    created_at: str = ""

    def __post_init__(self) -> None:
        if not self.created_at:
            self.created_at = datetime.now(timezone.utc).isoformat()

    @property
    def total_questions(self) -> int:
        return len(self.questions)

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "questions": [q.to_dict() for q in self.questions],
            "level": self.level.value,
            "passing_score": self.passing_score,
            "total_questions": self.total_questions,
            "created_at": self.created_at,
        }


@dataclass
class CurriculumModule:
    """A module entry in a curriculum plan."""

    module_type: TrainingModuleType
    title: str
    level: TrainingLevel
    estimated_minutes: int
    order: int
    is_required: bool = True
    description: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "module_type": self.module_type.value,
            "title": self.title,
            "level": self.level.value,
            "estimated_minutes": self.estimated_minutes,
            "order": self.order,
            "is_required": self.is_required,
            "description": self.description,
        }


@dataclass
class Curriculum:
    """A training curriculum plan."""

    title: str
    level: TrainingLevel
    modules: list[CurriculumModule] = field(default_factory=list)
    total_estimated_minutes: int = 0
    created_at: str = ""
    description: str = ""

    def __post_init__(self) -> None:
        if not self.created_at:
            self.created_at = datetime.now(timezone.utc).isoformat()
        if not self.total_estimated_minutes and self.modules:
            self.total_estimated_minutes = sum(
                m.estimated_minutes for m in self.modules
            )

    @property
    def total_hours(self) -> float:
        return round(self.total_estimated_minutes / 60, 1)

    @property
    def required_modules(self) -> list[CurriculumModule]:
        return [m for m in self.modules if m.is_required]

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "level": self.level.value,
            "modules": [m.to_dict() for m in self.modules],
            "total_estimated_minutes": self.total_estimated_minutes,
            "total_hours": self.total_hours,
            "total_modules": len(self.modules),
            "required_modules": len(self.required_modules),
            "created_at": self.created_at,
            "description": self.description,
        }


@dataclass
class TrainingMaterial:
    """Complete training material output."""

    title: str
    modules: list[TrainingModule] = field(default_factory=list)
    level: TrainingLevel = TrainingLevel.BEGINNER
    findings_count: int = 0
    created_at: str = ""

    def __post_init__(self) -> None:
        if not self.created_at:
            self.created_at = datetime.now(timezone.utc).isoformat()

    @property
    def total_sections(self) -> int:
        return sum(len(m.sections) for m in self.modules)

    @property
    def estimated_minutes(self) -> int:
        return sum(m.estimated_minutes for m in self.modules)

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "modules": [m.to_dict() for m in self.modules],
            "level": self.level.value,
            "findings_count": self.findings_count,
            "total_modules": len(self.modules),
            "total_sections": self.total_sections,
            "estimated_minutes": self.estimated_minutes,
            "created_at": self.created_at,
        }


# ---------------------------------------------------------------------------
# Finding Classification Helpers
# ---------------------------------------------------------------------------

_FINDING_CATEGORY_MAP: dict[str, TrainingModuleType] = {
    "CMD": TrainingModuleType.SECURE_CODING,
    "INJ": TrainingModuleType.SECURE_CODING,
    "SQL": TrainingModuleType.SECURE_CODING,
    "XSS": TrainingModuleType.SECURE_CODING,
    "SSRF": TrainingModuleType.SECURE_CODING,
    "PATH": TrainingModuleType.SECURE_CODING,
    "LEAK": TrainingModuleType.AWARENESS,
    "CRED": TrainingModuleType.AWARENESS,
    "KEY": TrainingModuleType.AWARENESS,
    "SEC": TrainingModuleType.AWARENESS,
    "DATA": TrainingModuleType.AWARENESS,
    "MCP": TrainingModuleType.MCP_SECURITY,
    "TOOL": TrainingModuleType.TOOL_SAFETY,
    "PERM": TrainingModuleType.TOOL_SAFETY,
    "AUTH": TrainingModuleType.TOOL_SAFETY,
    "PRIV": TrainingModuleType.TOOL_SAFETY,
    "OWASP": TrainingModuleType.OWASP,
    "DEP": TrainingModuleType.OWASP,
    "SUPPLY": TrainingModuleType.OWASP,
    "CONFIG": TrainingModuleType.MCP_SECURITY,
    "DRIFT": TrainingModuleType.MCP_SECURITY,
}


def _classify_finding_module(rule_id: str) -> TrainingModuleType:
    """Classify a finding into a training module type."""
    prefix = rule_id.split("-")[0].upper() if "-" in rule_id else rule_id.upper()
    if prefix in _FINDING_CATEGORY_MAP:
        return _FINDING_CATEGORY_MAP[prefix]
    for key, val in _FINDING_CATEGORY_MAP.items():
        if prefix.startswith(key):
            return val
    return TrainingModuleType.AWARENESS


def _severity_to_level(severity: str) -> TrainingLevel:
    """Map severity to suggested training level."""
    sev = severity.upper()
    if sev in ("CRITICAL", "HIGH"):
        return TrainingLevel.ADVANCED
    if sev == "MEDIUM":
        return TrainingLevel.INTERMEDIATE
    return TrainingLevel.BEGINNER


def get_module_types() -> dict[str, TrainingModuleType]:
    """Get all available module types."""
    return {m.value: m for m in TrainingModuleType}


# ---------------------------------------------------------------------------
# Training Content Builders
# ---------------------------------------------------------------------------

def _build_awareness_sections(
    findings: list[dict[str, Any]],
    level: TrainingLevel,
) -> list[TrainingSection]:
    """Build awareness training sections from findings."""
    sections = [
        TrainingSection(
            title="Understanding Security Findings",
            content=(
                "Security findings are potential vulnerabilities detected "
                "in your MCP server code. Each finding has a severity level "
                "(CRITICAL, HIGH, MEDIUM, LOW, INFO) and a specific rule ID "
                "that identifies the type of issue."
            ),
            key_points=[
                "Always address CRITICAL and HIGH findings first",
                "Never store credentials in source code",
                "Apply the principle of least privilege",
            ],
        ),
        TrainingSection(
            title="Credential Security",
            content=(
                "Never hardcode API keys, passwords, or tokens in source "
                "code. Use environment variables or secret managers. "
                "Rotate credentials regularly and audit access."
            ),
            bad_example='API_KEY = "sk-1234567890abcdef"',
            good_example='API_KEY = os.environ["API_KEY"]',
            key_points=[
                "Use environment variables for secrets",
                "Rotate credentials regularly",
                "Use .gitignore for sensitive files",
            ],
        ),
    ]

    if level.depth >= 2:
        sections.append(TrainingSection(
            title="Data Classification",
            content=(
                "Classify data by sensitivity: public, internal, "
                "confidential, restricted. Apply appropriate protections "
                "based on classification level."
            ),
            key_points=[
                "Classify data before processing",
                "Apply encryption for confidential data",
                "Log access to restricted data",
            ],
        ))

    if level.depth >= 3:
        sections.append(TrainingSection(
            title="Threat Modeling",
            content=(
                "Use STRIDE methodology to identify threats: Spoofing, "
                "Tampering, Repudiation, Information Disclosure, "
                "Denial of Service, Elevation of Privilege."
            ),
            key_points=[
                "Identify assets and trust boundaries",
                "Map attack surfaces",
                "Prioritize mitigations by risk",
            ],
            references=["OWASP Threat Modeling Cheat Sheet"],
        ))

    # Add finding-specific sections
    seen_rules: set[str] = set()
    for f in findings:
        rule_id = f.get("rule_id", "")
        if rule_id in seen_rules:
            continue
        seen_rules.add(rule_id)
        if _classify_finding_module(rule_id) == TrainingModuleType.AWARENESS:
            sections.append(TrainingSection(
                title=f"Finding: {f.get('title', rule_id)}",
                content=f.get("description", "Security issue detected."),
                code_example=f.get("code_snippet", ""),
                key_points=[
                    f.get("recommendation", "Review and fix this issue"),
                ],
            ))
            if len(sections) > 8:
                break

    return sections


def _build_secure_coding_sections(
    findings: list[dict[str, Any]],
    level: TrainingLevel,
) -> list[TrainingSection]:
    """Build secure coding training sections."""
    sections = [
        TrainingSection(
            title="Input Validation",
            content=(
                "Always validate and sanitize user input before processing. "
                "Never trust input from external sources, including MCP "
                "tool parameters, resource URIs, and prompt content."
            ),
            bad_example='subprocess.run(f"ls {user_input}", shell=True)',
            good_example=(
                'subprocess.run(["ls", user_input], shell=False)\n'
                '# Or better: validate user_input against allowlist'
            ),
            key_points=[
                "Validate all input at trust boundaries",
                "Use allowlists over denylists",
                "Avoid shell=True in subprocess calls",
                "Use parameterized queries for databases",
            ],
        ),
        TrainingSection(
            title="Output Encoding",
            content=(
                "Encode output appropriately for the context: HTML encoding "
                "for web output, SQL escaping for queries, shell escaping "
                "for commands."
            ),
            key_points=[
                "HTML-encode for web contexts",
                "Use parameterized queries",
                "Never concatenate user input into commands",
            ],
        ),
    ]

    if level.depth >= 2:
        sections.append(TrainingSection(
            title="Error Handling",
            content=(
                "Handle errors gracefully without exposing internal details. "
                "Log errors securely and return generic messages to users."
            ),
            bad_example=(
                'except Exception as e:\n'
                '    return {"error": str(e), "stack": traceback.format_exc()}'
            ),
            good_example=(
                'except Exception:\n'
                '    logger.exception("Operation failed")\n'
                '    return {"error": "An internal error occurred"}'
            ),
            key_points=[
                "Never expose stack traces to users",
                "Log exceptions with context",
                "Use generic error messages in responses",
            ],
        ))

    if level.depth >= 3:
        sections.append(TrainingSection(
            title="Secure Design Patterns",
            content=(
                "Apply security patterns: input validation at boundaries, "
                "defense in depth, fail-safe defaults, least privilege, "
                "and separation of concerns."
            ),
            key_points=[
                "Defense in depth: multiple layers of security",
                "Fail-safe defaults: deny by default",
                "Complete mediation: check every access",
                "Economy of mechanism: keep it simple",
            ],
            references=["Saltzer & Schroeder Design Principles"],
        ))

    # Add finding-specific sections
    seen_rules: set[str] = set()
    for f in findings:
        rule_id = f.get("rule_id", "")
        if rule_id in seen_rules:
            continue
        seen_rules.add(rule_id)
        if _classify_finding_module(rule_id) == TrainingModuleType.SECURE_CODING:
            sections.append(TrainingSection(
                title=f"Finding: {f.get('title', rule_id)}",
                content=f.get("description", "Coding issue detected."),
                code_example=f.get("code_snippet", ""),
                key_points=[
                    f.get("recommendation", "Review and fix this issue"),
                ],
            ))
            if len(sections) > 8:
                break

    return sections


def _build_mcp_security_sections(
    findings: list[dict[str, Any]],
    level: TrainingLevel,
) -> list[TrainingSection]:
    """Build MCP security training sections."""
    sections = [
        TrainingSection(
            title="MCP Security Fundamentals",
            content=(
                "The Model Context Protocol (MCP) allows AI models to "
                "interact with external tools and resources. Security is "
                "critical because MCP servers often have access to "
                "sensitive systems and data."
            ),
            key_points=[
                "MCP tools can execute arbitrary actions",
                "Resource URIs may access sensitive data",
                "Prompts can be injected with malicious content",
                "Transport layer security is essential",
            ],
        ),
        TrainingSection(
            title="MCP Tool Security",
            content=(
                "Each MCP tool should have explicit permission boundaries. "
                "Validate all tool parameters, implement rate limiting, "
                "and log all tool invocations."
            ),
            key_points=[
                "Define clear permission boundaries per tool",
                "Validate all tool input parameters",
                "Implement rate limiting",
                "Log all tool invocations for auditing",
            ],
        ),
    ]

    if level.depth >= 2:
        sections.append(TrainingSection(
            title="Prompt Injection Defense",
            content=(
                "Prompt injection attacks attempt to manipulate AI model "
                "behavior through crafted inputs. Defend against both "
                "direct and indirect injection vectors."
            ),
            key_points=[
                "Separate system and user prompts clearly",
                "Validate prompt content before processing",
                "Implement output filtering",
                "Use structured data instead of free text where possible",
            ],
        ))

    if level.depth >= 3:
        sections.append(TrainingSection(
            title="MCP Transport Security",
            content=(
                "Secure the transport layer: use TLS for all connections, "
                "implement mutual authentication, validate message integrity, "
                "and handle connection lifecycle securely."
            ),
            key_points=[
                "Use TLS 1.3 for all transport",
                "Implement mutual authentication",
                "Validate message integrity",
                "Handle reconnection securely",
            ],
            references=["MCP Protocol Specification"],
        ))

    return sections


def _build_owasp_sections(
    findings: list[dict[str, Any]],
    level: TrainingLevel,
) -> list[TrainingSection]:
    """Build OWASP-focused training sections."""
    sections = [
        TrainingSection(
            title="OWASP Top 10 for MCP Servers",
            content=(
                "The OWASP Top 10 maps to MCP servers: injection, broken "
                "authentication, sensitive data exposure, insecure "
                "deserialization, and more apply to MCP contexts."
            ),
            key_points=[
                "A01: Broken Access Control",
                "A02: Cryptographic Failures",
                "A03: Injection",
                "A06: Vulnerable Components",
                "A09: Security Logging Failures",
            ],
        ),
    ]

    if level.depth >= 2:
        sections.append(TrainingSection(
            title="Supply Chain Security",
            content=(
                "Dependencies can introduce vulnerabilities. Audit all "
                "dependencies, use lockfiles, and monitor for known "
                "vulnerabilities and typosquatting."
            ),
            key_points=[
                "Pin dependency versions",
                "Use SBOM for dependency tracking",
                "Monitor for CVEs in dependencies",
                "Watch for typosquatting attacks",
            ],
        ))

    if level.depth >= 3:
        sections.append(TrainingSection(
            title="Security Logging & Monitoring",
            content=(
                "Implement comprehensive security logging: log authentication "
                "events, authorization decisions, input validation failures, "
                "and all tool invocations."
            ),
            key_points=[
                "Log security-relevant events",
                "Protect log integrity",
                "Set up alerting for anomalies",
                "Retain logs per compliance requirements",
            ],
        ))

    return sections


def _build_tool_safety_sections(
    findings: list[dict[str, Any]],
    level: TrainingLevel,
) -> list[TrainingSection]:
    """Build tool safety training sections."""
    sections = [
        TrainingSection(
            title="Tool Permission Model",
            content=(
                "Each MCP tool should operate under the principle of least "
                "privilege. Define explicit permission boundaries and "
                "validate all actions against an access control policy."
            ),
            key_points=[
                "Apply least privilege to every tool",
                "Define explicit permission boundaries",
                "Validate tool actions against policy",
                "Audit tool usage regularly",
            ],
        ),
        TrainingSection(
            title="Safe Tool Invocation",
            content=(
                "Validate all parameters before tool execution. Implement "
                "timeouts, resource limits, and rollback mechanisms. "
                "Never allow tools to modify their own permissions."
            ),
            bad_example='# Tool can write to any path\ntool.write(path=user_input)',
            good_example=(
                '# Validate path against allowlist\n'
                'if not is_allowed_path(path):\n'
                '    raise PermissionError("Path not allowed")\n'
                'tool.write(path=validated_path)'
            ),
            key_points=[
                "Validate parameters against schemas",
                "Implement execution timeouts",
                "Apply resource limits",
                "Never self-modify permissions",
            ],
        ),
    ]

    if level.depth >= 2:
        sections.append(TrainingSection(
            title="Confused Deputy Prevention",
            content=(
                "The confused deputy problem occurs when a trusted tool "
                "is tricked into performing unauthorized actions. Implement "
                "proper authorization checks at every action point."
            ),
            key_points=[
                "Verify authorization at point of action",
                "Don't rely on caller identity alone",
                "Implement capability-based security",
                "Validate intent, not just identity",
            ],
        ))

    return sections


# ---------------------------------------------------------------------------
# Module Builder
# ---------------------------------------------------------------------------

def _build_module(
    module_type: TrainingModuleType,
    findings: list[dict[str, Any]],
    level: TrainingLevel,
) -> TrainingModule:
    """Build a training module for a given type."""
    builders = {
        TrainingModuleType.AWARENESS: _build_awareness_sections,
        TrainingModuleType.SECURE_CODING: _build_secure_coding_sections,
        TrainingModuleType.MCP_SECURITY: _build_mcp_security_sections,
        TrainingModuleType.OWASP: _build_owasp_sections,
        TrainingModuleType.TOOL_SAFETY: _build_tool_safety_sections,
    }

    builder = builders.get(module_type, _build_awareness_sections)
    sections = builder(findings, level)

    # Adjust estimated time by level
    base = module_type.estimated_minutes
    multiplier = {
        TrainingLevel.BEGINNER: 1.0,
        TrainingLevel.INTERMEDIATE: 1.3,
        TrainingLevel.ADVANCED: 1.6,
    }
    est = int(base * multiplier.get(level, 1.0))

    objectives = {
        TrainingModuleType.AWARENESS: [
            "Understand security finding severity levels",
            "Identify credential exposure risks",
            "Apply basic security hygiene",
        ],
        TrainingModuleType.SECURE_CODING: [
            "Implement proper input validation",
            "Apply output encoding techniques",
            "Use secure coding patterns",
        ],
        TrainingModuleType.MCP_SECURITY: [
            "Understand MCP security architecture",
            "Implement tool permission boundaries",
            "Defend against prompt injection",
        ],
        TrainingModuleType.OWASP: [
            "Map OWASP Top 10 to MCP context",
            "Identify common vulnerability patterns",
            "Apply security controls for each category",
        ],
        TrainingModuleType.TOOL_SAFETY: [
            "Implement least-privilege tool access",
            "Validate tool parameters securely",
            "Prevent confused deputy attacks",
        ],
    }

    return TrainingModule(
        module_type=module_type,
        title=module_type.label,
        description=f"{module_type.label} training for MCP security.",
        sections=sections,
        level=level,
        estimated_minutes=est,
        objectives=objectives.get(module_type, []),
    )


# ---------------------------------------------------------------------------
# Training Generation (main API)
# ---------------------------------------------------------------------------

def generate_training(
    findings: list[dict[str, Any]],
    level: TrainingLevel | str = TrainingLevel.BEGINNER,
    module_types: list[TrainingModuleType] | None = None,
) -> TrainingMaterial:
    """Generate personalized training material from findings.

    Parameters
    ----------
    findings : list[dict]
        List of finding dicts with rule_id, severity, etc.
    level : TrainingLevel | str
        Training difficulty level.
    module_types : list[TrainingModuleType] | None
        Specific modules to include; None = auto-detect from findings.

    Returns
    -------
    TrainingMaterial
        Complete training material with modules and sections.
    """
    if isinstance(level, str):
        level = TrainingLevel.from_string(level)

    if module_types is None:
        # Auto-detect needed modules from findings
        needed: set[TrainingModuleType] = set()
        for f in findings:
            needed.add(_classify_finding_module(f.get("rule_id", "")))
        if not needed:
            needed = {TrainingModuleType.AWARENESS, TrainingModuleType.MCP_SECURITY}
        module_types = sorted(needed, key=lambda m: m.value)

    modules = []
    for mt in module_types:
        # Filter findings relevant to this module
        relevant = [
            f for f in findings
            if _classify_finding_module(f.get("rule_id", "")) == mt
        ]
        mod = _build_module(mt, relevant, level)
        modules.append(mod)

    return TrainingMaterial(
        title=f"MCP Security Training ({level.label})",
        modules=modules,
        level=level,
        findings_count=len(findings),
    )


# ---------------------------------------------------------------------------
# Quiz Generation
# ---------------------------------------------------------------------------

# Question templates by module type
_QUESTION_TEMPLATES: dict[TrainingModuleType, list[dict[str, Any]]] = {
    TrainingModuleType.AWARENESS: [
        {
            "text": "What is the safest way to store API credentials?",
            "answers": [
                {"text": "Hardcode in source code", "correct": False,
                 "explanation": "Hardcoded credentials can be exposed in version control."},
                {"text": "Environment variables or secret manager", "correct": True,
                 "explanation": "Secrets should be stored outside the codebase."},
                {"text": "In a comment for reference", "correct": False,
                 "explanation": "Comments are visible in source code."},
                {"text": "In a public config file", "correct": False,
                 "explanation": "Public config files may be exposed."},
            ],
            "category": "credential_security",
        },
        {
            "text": "Which severity level should be addressed first?",
            "answers": [
                {"text": "INFO", "correct": False,
                 "explanation": "INFO findings are informational only."},
                {"text": "LOW", "correct": False,
                 "explanation": "LOW findings are lower priority."},
                {"text": "CRITICAL", "correct": True,
                 "explanation": "CRITICAL findings pose immediate risk."},
                {"text": "MEDIUM", "correct": False,
                 "explanation": "MEDIUM findings should be addressed, but CRITICAL first."},
            ],
            "category": "severity_triage",
        },
    ],
    TrainingModuleType.SECURE_CODING: [
        {
            "text": "What is the primary defense against command injection?",
            "answers": [
                {"text": "Avoid subprocess calls entirely", "correct": False,
                 "explanation": "Subprocess is sometimes needed; proper use is key."},
                {"text": "Use shell=True with escaping", "correct": False,
                 "explanation": "shell=True is inherently risky even with escaping."},
                {"text": "Use parameterized commands (shell=False)", "correct": True,
                 "explanation": "Parameterized commands prevent injection."},
                {"text": "Filter semicolons from input", "correct": False,
                 "explanation": "Denylists are easily bypassed."},
            ],
            "category": "injection_prevention",
        },
        {
            "text": "Which approach is best for SQL injection prevention?",
            "answers": [
                {"text": "String concatenation with escaping", "correct": False,
                 "explanation": "Manual escaping is error-prone."},
                {"text": "Parameterized queries / prepared statements", "correct": True,
                 "explanation": "Parameterized queries separate code from data."},
                {"text": "Input length validation only", "correct": False,
                 "explanation": "Length validation alone doesn't prevent injection."},
                {"text": "Remove all special characters", "correct": False,
                 "explanation": "Stripping characters can break legitimate input."},
            ],
            "category": "sql_injection",
        },
    ],
    TrainingModuleType.MCP_SECURITY: [
        {
            "text": "What is the biggest risk with MCP tools?",
            "answers": [
                {"text": "They are slow", "correct": False,
                 "explanation": "Performance is not a security concern."},
                {"text": "They can execute arbitrary actions", "correct": True,
                 "explanation": "MCP tools can interact with systems, making access control critical."},
                {"text": "They use too much memory", "correct": False,
                 "explanation": "Resource usage is an operational concern."},
                {"text": "They need network access", "correct": False,
                 "explanation": "Network access is a feature, not inherently a risk."},
            ],
            "category": "mcp_fundamentals",
        },
    ],
    TrainingModuleType.OWASP: [
        {
            "text": "Which OWASP category covers injection attacks?",
            "answers": [
                {"text": "A01: Broken Access Control", "correct": False,
                 "explanation": "A01 covers authorization issues."},
                {"text": "A03: Injection", "correct": True,
                 "explanation": "A03 specifically covers injection vulnerabilities."},
                {"text": "A06: Vulnerable Components", "correct": False,
                 "explanation": "A06 covers dependency vulnerabilities."},
                {"text": "A09: Security Logging Failures", "correct": False,
                 "explanation": "A09 covers logging deficiencies."},
            ],
            "category": "owasp_top10",
        },
    ],
    TrainingModuleType.TOOL_SAFETY: [
        {
            "text": "What principle should guide MCP tool permissions?",
            "answers": [
                {"text": "Maximum privilege for convenience", "correct": False,
                 "explanation": "Maximum privilege increases attack surface."},
                {"text": "Least privilege", "correct": True,
                 "explanation": "Tools should have minimal necessary permissions."},
                {"text": "No permissions needed", "correct": False,
                 "explanation": "Some permissions are needed for functionality."},
                {"text": "Admin access for all tools", "correct": False,
                 "explanation": "Admin access violates least privilege."},
            ],
            "category": "tool_permissions",
        },
    ],
}


def _question_id(text: str, idx: int) -> str:
    """Generate a stable question ID."""
    h = hashlib.md5(text.encode()).hexdigest()[:8]
    return f"Q{idx:03d}_{h}"


def _build_finding_question(
    finding: dict[str, Any],
    idx: int,
    level: TrainingLevel,
) -> Question:
    """Build a question from a specific finding."""
    rule_id = finding.get("rule_id", "UNKNOWN-001")
    title = finding.get("title", "Security Issue")
    description = finding.get("description", "")
    recommendation = finding.get("recommendation", "Fix this issue")

    text = f"A scan found '{title}' ({rule_id}). What is the best response?"

    answers = [
        Answer(
            text=recommendation if recommendation else "Apply the recommended fix",
            is_correct=True,
            explanation="Follow the scanner's recommendation to address the root cause.",
        ),
        Answer(
            text="Ignore the finding",
            is_correct=False,
            explanation="Ignoring findings leaves vulnerabilities unaddressed.",
        ),
        Answer(
            text="Disable the scanning rule",
            is_correct=False,
            explanation="Disabling rules hides problems instead of fixing them.",
        ),
        Answer(
            text="Add a comment explaining the risk",
            is_correct=False,
            explanation="Comments don't mitigate security risks.",
        ),
    ]

    return Question(
        question_id=_question_id(text, idx),
        text=text,
        answers=answers,
        difficulty=level,
        category=_classify_finding_module(rule_id).value,
        related_rule_id=rule_id,
        explanation=description,
    )


def generate_quiz(
    findings: list[dict[str, Any]],
    num_questions: int = 10,
    level: TrainingLevel | str = TrainingLevel.BEGINNER,
) -> Quiz:
    """Generate a quiz from findings.

    Parameters
    ----------
    findings : list[dict]
        Finding dicts with rule_id, severity, etc.
    num_questions : int
        Number of questions to generate.
    level : TrainingLevel | str
        Quiz difficulty level.

    Returns
    -------
    Quiz
        Quiz with multiple-choice questions.
    """
    if isinstance(level, str):
        level = TrainingLevel.from_string(level)

    questions: list[Question] = []
    idx = 1

    # First: template questions from relevant modules
    modules_seen: set[TrainingModuleType] = set()
    for f in findings:
        mt = _classify_finding_module(f.get("rule_id", ""))
        modules_seen.add(mt)

    if not modules_seen:
        modules_seen = {TrainingModuleType.AWARENESS}

    for mt in sorted(modules_seen, key=lambda m: m.value):
        templates = _QUESTION_TEMPLATES.get(mt, [])
        for tmpl in templates:
            if len(questions) >= num_questions:
                break
            answers = [
                Answer(
                    text=a["text"],
                    is_correct=a["correct"],
                    explanation=a.get("explanation", ""),
                )
                for a in tmpl["answers"]
            ]
            q = Question(
                question_id=_question_id(tmpl["text"], idx),
                text=tmpl["text"],
                answers=answers,
                difficulty=level,
                category=tmpl.get("category", mt.value),
            )
            questions.append(q)
            idx += 1

    # Second: finding-specific questions
    seen_rules: set[str] = set()
    for f in findings:
        if len(questions) >= num_questions:
            break
        rule_id = f.get("rule_id", "")
        if rule_id in seen_rules:
            continue
        seen_rules.add(rule_id)
        q = _build_finding_question(f, idx, level)
        questions.append(q)
        idx += 1

    # Limit to requested number
    questions = questions[:num_questions]

    return Quiz(
        title=f"MCP Security Quiz ({level.label})",
        questions=questions,
        level=level,
        passing_score=0.7,
    )


# ---------------------------------------------------------------------------
# Curriculum Generation
# ---------------------------------------------------------------------------

def generate_curriculum(
    findings: list[dict[str, Any]],
    level: TrainingLevel | str = TrainingLevel.BEGINNER,
) -> Curriculum:
    """Generate a training curriculum plan from findings.

    Parameters
    ----------
    findings : list[dict]
        Finding dicts.
    level : TrainingLevel | str
        Target training level.

    Returns
    -------
    Curriculum
        Structured curriculum plan with module order and durations.
    """
    if isinstance(level, str):
        level = TrainingLevel.from_string(level)

    # Determine needed modules from findings
    needed: set[TrainingModuleType] = set()
    for f in findings:
        needed.add(_classify_finding_module(f.get("rule_id", "")))

    if not needed:
        needed = {TrainingModuleType.AWARENESS, TrainingModuleType.MCP_SECURITY}

    # Standard module order
    module_order = [
        TrainingModuleType.AWARENESS,
        TrainingModuleType.SECURE_CODING,
        TrainingModuleType.MCP_SECURITY,
        TrainingModuleType.OWASP,
        TrainingModuleType.TOOL_SAFETY,
    ]

    modules: list[CurriculumModule] = []
    order = 1
    for mt in module_order:
        is_needed = mt in needed
        multiplier = {
            TrainingLevel.BEGINNER: 1.0,
            TrainingLevel.INTERMEDIATE: 1.3,
            TrainingLevel.ADVANCED: 1.6,
        }
        est = int(mt.estimated_minutes * multiplier.get(level, 1.0))
        modules.append(CurriculumModule(
            module_type=mt,
            title=mt.label,
            level=level,
            estimated_minutes=est,
            order=order,
            is_required=is_needed,
            description=f"{mt.label} training for MCP security.",
        ))
        order += 1

    total = sum(m.estimated_minutes for m in modules)

    return Curriculum(
        title=f"MCP Security Curriculum ({level.label})",
        level=level,
        modules=modules,
        total_estimated_minutes=total,
        description=(
            f"Training plan covering {len(modules)} modules at "
            f"{level.label} level. Based on {len(findings)} findings."
        ),
    )


# ---------------------------------------------------------------------------
# Format: JSON
# ---------------------------------------------------------------------------

def format_training_json(material: TrainingMaterial) -> str:
    """Format training material as JSON."""
    return json.dumps(material.to_dict(), indent=2)


def format_quiz_json(quiz: Quiz) -> str:
    """Format quiz as JSON."""
    return json.dumps(quiz.to_dict(), indent=2)


def format_curriculum_json(curriculum: Curriculum) -> str:
    """Format curriculum as JSON."""
    return json.dumps(curriculum.to_dict(), indent=2)


# ---------------------------------------------------------------------------
# Format: Text
# ---------------------------------------------------------------------------

def format_training_text(material: TrainingMaterial) -> str:
    """Format training material as human-readable text."""
    lines: list[str] = []
    lines.append(f"{'=' * 70}")
    lines.append(f"  {material.title}")
    lines.append(f"  Level: {material.level.label}")
    lines.append(f"  Modules: {len(material.modules)}")
    lines.append(f"  Est. Duration: {material.estimated_minutes} min")
    lines.append(f"{'=' * 70}")
    lines.append("")

    for mod in material.modules:
        lines.append(f"--- {mod.title} ({mod.estimated_minutes} min) ---")
        if mod.objectives:
            lines.append("  Objectives:")
            for obj in mod.objectives:
                lines.append(f"    - {obj}")
        lines.append("")

        for sec in mod.sections:
            lines.append(f"  ## {sec.title}")
            lines.append(f"  {sec.content}")
            if sec.bad_example:
                lines.append(f"  BAD:  {sec.bad_example}")
            if sec.good_example:
                lines.append(f"  GOOD: {sec.good_example}")
            if sec.key_points:
                lines.append("  Key points:")
                for kp in sec.key_points:
                    lines.append(f"    * {kp}")
            lines.append("")

    lines.append(f"\nGenerated: {material.created_at}")
    return "\n".join(lines)


def format_quiz_text(quiz: Quiz) -> str:
    """Format quiz as human-readable text."""
    lines: list[str] = []
    lines.append(f"{'=' * 70}")
    lines.append(f"  {quiz.title}")
    lines.append(f"  Questions: {quiz.total_questions}")
    lines.append(f"  Passing Score: {quiz.passing_score:.0%}")
    lines.append(f"{'=' * 70}")
    lines.append("")

    for i, q in enumerate(quiz.questions, 1):
        lines.append(f"  Q{i}. {q.text}")
        for j, a in enumerate(q.answers):
            marker = "  *" if a.is_correct else "   "
            letter = chr(ord("A") + j)
            lines.append(f"  {marker} {letter}) {a.text}")
        lines.append("")

    return "\n".join(lines)


def format_curriculum_text(curriculum: Curriculum) -> str:
    """Format curriculum as human-readable text."""
    lines: list[str] = []
    lines.append(f"{'=' * 70}")
    lines.append(f"  {curriculum.title}")
    lines.append(f"  Level: {curriculum.level.label}")
    lines.append(f"  Total: {curriculum.total_estimated_minutes} min ({curriculum.total_hours}h)")
    lines.append(f"{'=' * 70}")
    lines.append("")

    for mod in curriculum.modules:
        req = "[REQUIRED]" if mod.is_required else "[OPTIONAL]"
        lines.append(
            f"  {mod.order}. {mod.title} — {mod.estimated_minutes} min {req}"
        )

    lines.append(f"\nRequired modules: {len(curriculum.required_modules)}")
    lines.append(f"Generated: {curriculum.created_at}")
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Format: HTML
# ---------------------------------------------------------------------------

def format_training_html(material: TrainingMaterial) -> str:
    """Format training material as an HTML document."""
    e = html_mod.escape

    parts: list[str] = []
    parts.append("<!DOCTYPE html>")
    parts.append("<html lang='en'><head>")
    parts.append("<meta charset='UTF-8'>")
    parts.append(f"<title>{e(material.title)}</title>")
    parts.append("<style>")
    parts.append("""
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
       max-width: 900px; margin: 0 auto; padding: 20px;
       background: #f8f9fa; color: #212529; }
h1 { border-bottom: 3px solid #212529; padding-bottom: 10px; }
h2 { color: #495057; }
h3 { color: #6c757d; }
.module { background: #fff; border-radius: 8px; padding: 15px;
          margin: 15px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.section { padding: 10px; margin: 8px 0; border-left: 3px solid #007bff;
           background: #f8f9fa; border-radius: 4px; }
.code-bad { background: #f8d7da; padding: 8px; border-radius: 4px;
            font-family: monospace; margin: 5px 0; }
.code-good { background: #d4edda; padding: 8px; border-radius: 4px;
             font-family: monospace; margin: 5px 0; }
.key-points { background: #cce5ff; padding: 8px; border-radius: 4px;
              margin: 5px 0; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px;
         color: #fff; font-size: 0.85em; }
""")
    parts.append("</style></head><body>")

    parts.append(f"<h1>{e(material.title)}</h1>")
    parts.append(
        f"<p>Level: <span class='badge' style='background:#007bff'>"
        f"{e(material.level.label)}</span> | "
        f"Modules: {len(material.modules)} | "
        f"Est: {material.estimated_minutes} min</p>"
    )

    for mod in material.modules:
        parts.append(f"<div class='module'>")
        parts.append(f"<h2>{e(mod.title)}</h2>")
        parts.append(f"<p>{e(mod.description)}</p>")

        if mod.objectives:
            parts.append("<h3>Objectives</h3><ul>")
            for obj in mod.objectives:
                parts.append(f"<li>{e(obj)}</li>")
            parts.append("</ul>")

        for sec in mod.sections:
            parts.append(f"<div class='section'>")
            parts.append(f"<h3>{e(sec.title)}</h3>")
            parts.append(f"<p>{e(sec.content)}</p>")
            if sec.bad_example:
                parts.append(
                    f"<div class='code-bad'><strong>Bad:</strong><br>"
                    f"<code>{e(sec.bad_example)}</code></div>"
                )
            if sec.good_example:
                parts.append(
                    f"<div class='code-good'><strong>Good:</strong><br>"
                    f"<code>{e(sec.good_example)}</code></div>"
                )
            if sec.key_points:
                parts.append("<div class='key-points'><strong>Key Points:</strong><ul>")
                for kp in sec.key_points:
                    parts.append(f"<li>{e(kp)}</li>")
                parts.append("</ul></div>")
            parts.append("</div>")

        parts.append("</div>")

    parts.append(
        f"<p><em>Generated: {e(material.created_at)}</em></p>"
    )
    parts.append("</body></html>")

    return "\n".join(parts)
