"""
Pattern Schema Validation

Runtime validation for attack pattern JSON files. Catches malformed patterns,
missing fields, and invalid values when agents load them — before they cause
runtime crashes during an active penetration test.

Usage:
    from src.utils.pattern_schema import validate_patterns, validate_pattern_file

    # Validate patterns after loading
    patterns = json.load(f)
    errors = validate_patterns(patterns, filename="jailbreak_patterns.json")
    if errors:
        logger.warning("pattern_validation_errors", errors=errors)

    # Validate a file directly
    errors = validate_pattern_file("src/attack_library/jailbreak_patterns.json")
"""

import json
from pathlib import Path
from typing import Any

import structlog

logger = structlog.get_logger(__name__)

VALID_SEVERITIES = {"critical", "high", "medium", "low", "info"}

VALID_CONTENT_FIELDS = {
    "template",
    "prompt",
    "turns",
    "example",
    "examples",
    "payload_examples",
    "documents",
}


def extract_patterns(data: Any) -> list[dict]:
    """
    Extract pattern list from any pattern file format.

    Handles:
        - Top-level list (jailbreak_patterns.json, encoding_methods.json, etc.)
        - Dict with 'patterns' key (most OWASP files)
        - Dict with 'pattern_categories' key (advanced jailbreak)

    Args:
        data: Parsed JSON data (dict or list)

    Returns:
        List of pattern dicts
    """
    if isinstance(data, list):
        return data
    if isinstance(data, dict):
        if "patterns" in data:
            return data["patterns"]
        if "pattern_categories" in data:
            patterns = []
            for category in data["pattern_categories"].values():
                if isinstance(category, dict):
                    patterns.extend(category.get("patterns", []))
            return patterns
    return []


def validate_pattern(pattern: dict, index: int, filename: str = "") -> list[str]:
    """
    Validate a single pattern dict.

    Checks:
        - Has identifier (name or id)
        - Has content (template, prompt, turns, examples, documents)
        - Severity values are valid
        - Success indicators are lists of strings
        - Priority is int 1-5 if present
        - Template braces are balanced

    Args:
        pattern: Single pattern dict
        index: Pattern index in the file (for error messages)
        filename: Source filename (for error messages)

    Returns:
        List of error strings (empty if valid)
    """
    errors = []
    ctx = f"pattern #{index} in {filename}" if filename else f"pattern #{index}"

    # Must have identifier
    name = pattern.get("name", pattern.get("id"))
    if not name:
        errors.append(f"{ctx}: missing 'name' or 'id' field")
        name = f"#{index}"
    ctx = f"'{name}' in {filename}" if filename else f"'{name}'"

    # Must have content
    has_content = any(k in pattern for k in VALID_CONTENT_FIELDS)
    if not has_content:
        errors.append(f"{ctx}: no content field ({', '.join(sorted(VALID_CONTENT_FIELDS))})")

    # Validate severity
    for sev_field in ("severity", "severity_if_success"):
        if sev_field in pattern:
            sev = pattern[sev_field]
            if isinstance(sev, str) and sev.lower() not in VALID_SEVERITIES:
                errors.append(
                    f"{ctx}: invalid {sev_field}='{sev}', "
                    f"must be one of {sorted(VALID_SEVERITIES)}"
                )

    # Validate priority
    if "priority" in pattern:
        prio = pattern["priority"]
        if not isinstance(prio, int):
            errors.append(f"{ctx}: priority must be int, got {type(prio).__name__}")
        elif not 1 <= prio <= 5:
            errors.append(f"{ctx}: priority must be 1-5, got {prio}")

    # Validate success indicators
    for ind_field in ("success_indicators", "detection_indicators"):
        if ind_field in pattern:
            indicators = pattern[ind_field]
            if not isinstance(indicators, list):
                errors.append(f"{ctx}: {ind_field} must be list, got {type(indicators).__name__}")
            else:
                for j, ind in enumerate(indicators):
                    if not isinstance(ind, str):
                        errors.append(
                            f"{ctx}: {ind_field}[{j}] must be string, " f"got {type(ind).__name__}"
                        )

    # Validate template braces are balanced
    template = pattern.get("template", "")
    if template and isinstance(template, str):
        open_count = template.count("{")
        close_count = template.count("}")
        if open_count != close_count:
            errors.append(
                f"{ctx}: unbalanced braces in template " f"({open_count} open, {close_count} close)"
            )

    return errors


def validate_patterns(
    data: Any,
    filename: str = "",
    strict: bool = False,
) -> list[str]:
    """
    Validate all patterns in a loaded JSON structure.

    Args:
        data: Parsed JSON data (dict or list)
        filename: Source filename for error messages
        strict: If True, raise ValueError on first error

    Returns:
        List of error strings (empty if all valid)

    Raises:
        ValueError: If strict=True and validation errors found
    """
    patterns = extract_patterns(data)
    all_errors = []

    if not patterns:
        all_errors.append(f"{filename}: no patterns found")

    # Check for duplicate names
    names = [p.get("name", p.get("id", "")) for p in patterns]
    names = [n for n in names if n]
    seen = set()
    for name in names:
        if name in seen:
            all_errors.append(f"{filename}: duplicate pattern name '{name}'")
        seen.add(name)

    # Validate each pattern
    for i, pattern in enumerate(patterns):
        errors = validate_pattern(pattern, i, filename)
        all_errors.extend(errors)

    if strict and all_errors:
        raise ValueError(
            f"Pattern validation failed for {filename}:\n"
            + "\n".join(f"  - {e}" for e in all_errors)
        )

    return all_errors


def validate_pattern_file(filepath: str | Path) -> list[str]:
    """
    Validate a pattern JSON file from disk.

    Args:
        filepath: Path to JSON pattern file

    Returns:
        List of error strings (empty if valid)
    """
    filepath = Path(filepath)

    if not filepath.exists():
        return [f"File not found: {filepath}"]

    try:
        with open(filepath) as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        return [f"Invalid JSON in {filepath.name}: {e}"]

    return validate_patterns(data, filename=filepath.name)


def load_and_validate_patterns(
    filepath: str | Path,
    log_warnings: bool = True,
) -> list[dict]:
    """
    Load patterns from a JSON file with validation.

    Convenience function for agents to use instead of raw json.load.
    Logs warnings for validation issues but still returns patterns
    (graceful degradation).

    Args:
        filepath: Path to JSON pattern file
        log_warnings: Whether to log validation warnings

    Returns:
        List of pattern dicts (may be empty on error)

    Example:
        patterns = load_and_validate_patterns(
            Path(__file__).parent.parent / "attack_library" / "jailbreak_patterns.json"
        )
    """
    filepath = Path(filepath)

    if not filepath.exists():
        if log_warnings:
            logger.warning("pattern_file_not_found", path=str(filepath))
        return []

    try:
        with open(filepath) as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        if log_warnings:
            logger.error("pattern_file_invalid_json", path=str(filepath), error=str(e))
        return []

    errors = validate_patterns(data, filename=filepath.name)
    if errors and log_warnings:
        logger.warning(
            "pattern_validation_warnings",
            file=filepath.name,
            error_count=len(errors),
            errors=errors[:5],  # Log first 5 to avoid spam
        )

    return extract_patterns(data)
