"""
Engineering Validator

Validates interview answers against engineering knowledge and detects anti-patterns.
Provides:
- MaterialsDatabase: Common materials with properties
- AntiPatternDetector: Detects optimization setup mistakes
- EngineeringValidator: Main validation logic
"""

from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Any, Optional, Tuple
import json
import re
from difflib import SequenceMatcher


@dataclass
class Material:
    """Engineering material with properties."""
    id: str
    names: List[str]
    category: str
    properties: Dict[str, Any]
    notes: Optional[str] = None
    recommended_safety_factors: Optional[Dict[str, float]] = None

    @property
    def density(self) -> Optional[float]:
        return self.properties.get("density_kg_m3")

    @property
    def yield_stress(self) -> Optional[float]:
        return self.properties.get("yield_stress_mpa")

    @property
    def ultimate_stress(self) -> Optional[float]:
        return self.properties.get("ultimate_stress_mpa")

    @property
    def elastic_modulus(self) -> Optional[float]:
        return self.properties.get("elastic_modulus_gpa")

    def get_safe_stress(self, application: str = "static") -> Optional[float]:
        """Get safe stress limit with recommended safety factor."""
        if self.yield_stress is None:
            return None

        sf = 1.5  # Default
        if self.recommended_safety_factors:
            sf = self.recommended_safety_factors.get(application, 1.5)

        return self.yield_stress / sf


class MaterialsDatabase:
    """
    Database of common engineering materials and properties.

    Supports fuzzy name matching for user convenience.
    """

    def __init__(self, db_path: Optional[Path] = None):
        """
        Initialize materials database.

        Args:
            db_path: Path to materials JSON. Uses default if None.
        """
        if db_path is None:
            db_path = Path(__file__).parent / "schemas" / "materials_database.json"

        self.db_path = db_path
        self.materials: Dict[str, Material] = {}
        self._name_index: Dict[str, str] = {}  # name -> material_id

        self._load_database()

    def _load_database(self) -> None:
        """Load materials from JSON file."""
        if not self.db_path.exists():
            return

        with open(self.db_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        for mat_data in data.get("materials", []):
            material = Material(
                id=mat_data["id"],
                names=mat_data["names"],
                category=mat_data["category"],
                properties=mat_data["properties"],
                notes=mat_data.get("notes"),
                recommended_safety_factors=mat_data.get("recommended_safety_factors"),
            )
            self.materials[material.id] = material

            # Build name index
            for name in material.names:
                self._name_index[name.lower()] = material.id

    def get_material(self, name: str) -> Optional[Material]:
        """
        Look up material by name (supports fuzzy matching).

        Args:
            name: Material name (e.g., "Al 6061-T6", "aluminum", "steel 304")

        Returns:
            Material if found, None otherwise
        """
        name_lower = name.lower().strip()

        # Exact match
        if name_lower in self._name_index:
            return self.materials[self._name_index[name_lower]]

        # Try by ID
        if name_lower in self.materials:
            return self.materials[name_lower]

        # Fuzzy match
        best_match = None
        best_ratio = 0.6  # Minimum threshold

        for indexed_name, mat_id in self._name_index.items():
            ratio = SequenceMatcher(None, name_lower, indexed_name).ratio()
            if ratio > best_ratio:
                best_ratio = ratio
                best_match = mat_id

        if best_match:
            return self.materials[best_match]

        return None

    def get_yield_stress(self, material_name: str) -> Optional[float]:
        """Get yield stress for material in MPa."""
        material = self.get_material(material_name)
        return material.yield_stress if material else None

    def validate_stress_limit(
        self,
        material_name: str,
        limit: float,
        safety_factor: float = 1.0,
        application: str = "static"
    ) -> "ValidationResult":
        """
        Check if stress limit is reasonable for material.

        Args:
            material_name: Material name
            limit: Proposed stress limit in MPa
            safety_factor: Applied safety factor (if any)
            application: Application type (static, fatigue, aerospace)

        Returns:
            ValidationResult with status and message
        """
        material = self.get_material(material_name)

        if material is None:
            return ValidationResult(
                valid=True,
                message=f"Material '{material_name}' not found in database. Unable to validate stress limit.",
                severity="info"
            )

        if material.yield_stress is None:
            return ValidationResult(
                valid=True,
                message=f"Material '{material.id}' does not have yield stress defined (e.g., brittle material).",
                severity="info"
            )

        yield_stress = material.yield_stress
        effective_limit = limit * safety_factor if safety_factor > 1 else limit

        # Check various thresholds
        if effective_limit > material.ultimate_stress if material.ultimate_stress else yield_stress:
            return ValidationResult(
                valid=False,
                message=f"Stress limit ({limit} MPa) exceeds ultimate stress ({material.ultimate_stress or yield_stress} MPa) for {material.id}",
                severity="error",
                suggestion=f"Reduce stress limit to below {(material.ultimate_stress or yield_stress) / 1.5:.0f} MPa"
            )

        if effective_limit > yield_stress:
            return ValidationResult(
                valid=True,  # Warning, not error
                message=f"Stress limit ({limit} MPa) exceeds yield stress ({yield_stress} MPa) for {material.id}. This allows plastic deformation.",
                severity="warning",
                suggestion=f"Consider reducing to {yield_stress / 1.5:.0f} MPa (SF=1.5)"
            )

        # Get recommended safe stress
        safe_stress = material.get_safe_stress(application)
        if safe_stress and limit > safe_stress:
            rec_sf = material.recommended_safety_factors.get(application, 1.5) if material.recommended_safety_factors else 1.5
            return ValidationResult(
                valid=True,
                message=f"Stress limit ({limit} MPa) is {limit/yield_stress*100:.0f}% of yield. Recommended safety factor for {application}: {rec_sf}",
                severity="info",
                suggestion=f"Typical {application} limit: {safe_stress:.0f} MPa"
            )

        return ValidationResult(
            valid=True,
            message=f"Stress limit ({limit} MPa) is acceptable for {material.id} (yield: {yield_stress} MPa)",
            severity="ok"
        )

    def list_materials(self, category: Optional[str] = None) -> List[Material]:
        """List all materials, optionally filtered by category."""
        materials = list(self.materials.values())
        if category:
            materials = [m for m in materials if m.category == category]
        return materials


@dataclass
class ValidationResult:
    """Result of a validation check."""
    valid: bool
    message: str
    severity: str = "ok"  # ok, info, warning, error
    suggestion: Optional[str] = None
    field: Optional[str] = None

    def is_blocking(self) -> bool:
        """Check if this result blocks proceeding."""
        return self.severity == "error"


@dataclass
class AntiPattern:
    """Detected anti-pattern."""
    id: str
    name: str
    description: str
    severity: str  # error, warning, info
    fix_suggestion: Optional[str] = None
    auto_fix: Optional[Dict[str, Any]] = None
    acknowledged: bool = False

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "severity": self.severity,
            "fix_suggestion": self.fix_suggestion,
            "auto_fix": self.auto_fix,
            "acknowledged": self.acknowledged,
        }


class AntiPatternDetector:
    """
    Detects common optimization setup mistakes.

    Loads patterns from JSON and evaluates against interview state.
    """

    def __init__(self, patterns_path: Optional[Path] = None):
        """
        Initialize anti-pattern detector.

        Args:
            patterns_path: Path to patterns JSON. Uses default if None.
        """
        if patterns_path is None:
            patterns_path = Path(__file__).parent / "schemas" / "anti_patterns.json"

        self.patterns_path = patterns_path
        self.patterns: List[Dict[str, Any]] = []

        self._load_patterns()

    def _load_patterns(self) -> None:
        """Load patterns from JSON file."""
        if not self.patterns_path.exists():
            return

        with open(self.patterns_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        self.patterns = data.get("patterns", [])

    def check_all(self, state: "InterviewState", introspection: Dict[str, Any] = None) -> List[AntiPattern]:
        """
        Run all anti-pattern checks.

        Args:
            state: Current interview state
            introspection: Optional introspection results

        Returns:
            List of detected anti-patterns
        """
        detected = []
        context = self._build_context(state, introspection or {})

        for pattern in self.patterns:
            if self._evaluate_condition(pattern.get("condition", {}), context):
                detected.append(AntiPattern(
                    id=pattern["id"],
                    name=pattern["name"],
                    description=pattern["description"],
                    severity=pattern["severity"],
                    fix_suggestion=pattern.get("fix_suggestion"),
                    auto_fix=pattern.get("auto_fix"),
                ))

        return detected

    def _build_context(self, state: "InterviewState", introspection: Dict[str, Any]) -> Dict[str, Any]:
        """Build evaluation context from state and introspection."""
        answers = state.answers

        # Extract objectives as list of goal values
        objectives_list = []
        for obj in answers.get("objectives", []):
            if isinstance(obj, dict):
                objectives_list.append(obj.get("goal", ""))
            else:
                objectives_list.append(str(obj))

        # Add secondary objectives if present
        for obj in answers.get("objectives_secondary", []):
            if obj != "none":
                objectives_list.append(obj)

        return {
            "objectives": objectives_list,
            "constraints": answers.get("constraints", {}),
            "design_variables": answers.get("design_variables", []),
            "design_variable_count": len(answers.get("design_variables", [])),
            "analysis_types": answers.get("analysis_types", []),
            "solve_all_solutions": answers.get("solve_all_solutions", True),
            "n_trials": answers.get("n_trials", 100),
            "introspection": introspection,
            "material": introspection.get("material"),
            "baseline_violations": state.get_answer("baseline_violations"),
        }

    def _evaluate_condition(self, condition: Dict[str, Any], context: Dict[str, Any]) -> bool:
        """Evaluate a pattern condition against context."""
        if not condition:
            return False

        cond_type = condition.get("type", "")

        if cond_type == "and":
            return all(
                self._evaluate_condition(c, context)
                for c in condition.get("conditions", [])
            )

        elif cond_type == "or":
            return any(
                self._evaluate_condition(c, context)
                for c in condition.get("conditions", [])
            )

        elif cond_type == "not":
            inner = condition.get("condition", {})
            return not self._evaluate_condition(inner, context)

        elif cond_type == "contains":
            field_value = self._get_field(context, condition.get("field", ""))
            target = condition.get("value")
            if isinstance(field_value, list):
                return target in field_value
            return False

        elif cond_type == "not_contains":
            field_value = self._get_field(context, condition.get("field", ""))
            target = condition.get("value")
            if isinstance(field_value, list):
                return target not in field_value
            return True

        elif cond_type == "equals":
            field_value = self._get_field(context, condition.get("field", ""))
            return field_value == condition.get("value")

        elif cond_type == "empty":
            field_value = self._get_field(context, condition.get("field", ""))
            if field_value is None:
                return True
            if isinstance(field_value, (list, dict, str)):
                return len(field_value) == 0
            return False

        elif cond_type == "exists":
            field_value = self._get_field(context, condition.get("field", ""))
            return field_value is not None

        elif cond_type == "not_exists":
            field_value = self._get_field(context, condition.get("field", ""))
            return field_value is None

        elif cond_type == "greater_than":
            field_value = self._get_field(context, condition.get("field", ""))
            compare = condition.get("value")

            # Handle compare_to (field reference)
            if "compare_to" in condition:
                compare_ref = condition["compare_to"]
                if isinstance(compare_ref, dict):
                    # Dynamic calculation
                    if compare_ref.get("type") == "multiply":
                        base_value = self._get_field(context, compare_ref.get("field", ""))
                        if base_value is not None:
                            compare = base_value * compare_ref.get("value", 1)
                else:
                    compare = self._get_field(context, compare_ref)

            if field_value is not None and compare is not None:
                try:
                    return float(field_value) > float(compare)
                except (ValueError, TypeError):
                    return False
            return False

        elif cond_type == "less_than":
            field_value = self._get_field(context, condition.get("field", ""))
            compare = condition.get("value")

            if "compare_to" in condition:
                compare_ref = condition["compare_to"]
                if isinstance(compare_ref, dict):
                    if compare_ref.get("type") == "multiply":
                        base_value = self._get_field(context, compare_ref.get("field", ""))
                        if base_value is not None:
                            compare = base_value * compare_ref.get("value", 1)
                else:
                    compare = self._get_field(context, compare_ref)

            if field_value is not None and compare is not None:
                try:
                    return float(field_value) < float(compare)
                except (ValueError, TypeError):
                    return False
            return False

        elif cond_type == "count_greater_than":
            field_value = self._get_field(context, condition.get("field", ""))
            if isinstance(field_value, (list, dict)):
                return len(field_value) > condition.get("value", 0)
            return False

        elif cond_type == "count_equals":
            field_value = self._get_field(context, condition.get("field", ""))
            if isinstance(field_value, (list, dict)):
                return len(field_value) == condition.get("value", 0)
            return False

        elif cond_type == "any_of":
            # Check if any item in array matches a condition
            field_value = self._get_field(context, condition.get("field", ""))
            if not isinstance(field_value, list):
                return False
            check = condition.get("check", {})
            for item in field_value:
                if isinstance(item, dict):
                    item_context = {**context, "item": item}
                    if self._evaluate_condition(check, item_context):
                        return True
            return False

        elif cond_type == "ratio_greater_than":
            # For bounds checking
            fields = condition.get("field", [])
            if len(fields) == 2:
                val1 = self._get_field(context, f"item.{fields[0]}")
                val2 = self._get_field(context, f"item.{fields[1]}")
                if val1 and val2 and val2 != 0:
                    try:
                        return float(val1) / float(val2) > condition.get("value", 1)
                    except (ValueError, TypeError):
                        return False
            return False

        return False

    def _get_field(self, context: Dict[str, Any], field_path: str) -> Any:
        """Get a field value from context using dot notation."""
        if not field_path:
            return None

        parts = field_path.split(".")
        current = context

        for part in parts:
            if current is None:
                return None
            if isinstance(current, dict):
                current = current.get(part)
            else:
                return None

        return current


class EngineeringValidator:
    """
    Main engineering validator.

    Combines materials database and anti-pattern detection with
    additional validation logic.
    """

    def __init__(self):
        """Initialize validator with materials DB and anti-pattern detector."""
        self.materials_db = MaterialsDatabase()
        self.anti_patterns = AntiPatternDetector()

    def validate_constraint(
        self,
        constraint_type: str,
        value: float,
        material: Optional[str] = None,
        baseline: Optional[float] = None
    ) -> ValidationResult:
        """
        Validate a constraint value against engineering limits.

        Args:
            constraint_type: Type of constraint (stress, displacement, frequency)
            value: Constraint value
            material: Optional material name for property lookups
            baseline: Optional baseline value for feasibility check

        Returns:
            ValidationResult
        """
        if constraint_type == "stress" and material:
            return self.materials_db.validate_stress_limit(material, value)

        # Check against baseline if available
        if baseline is not None:
            if constraint_type in ["stress", "displacement"]:
                # Max constraint - baseline should be under limit
                if baseline > value:
                    return ValidationResult(
                        valid=True,
                        message=f"Baseline ({baseline:.2f}) exceeds limit ({value}). Optimization starts infeasible.",
                        severity="warning",
                        suggestion="Consider relaxing the constraint or improving the baseline design"
                    )
            elif constraint_type == "frequency":
                # Min constraint - baseline should be above limit
                if baseline < value:
                    return ValidationResult(
                        valid=True,
                        message=f"Baseline frequency ({baseline:.2f} Hz) is below limit ({value} Hz). Optimization starts infeasible.",
                        severity="warning",
                        suggestion="Consider relaxing the constraint"
                    )

        return ValidationResult(
            valid=True,
            message=f"Constraint {constraint_type} = {value} accepted",
            severity="ok"
        )

    def validate_bounds(
        self,
        parameter: str,
        min_value: float,
        max_value: float,
        current_value: Optional[float] = None
    ) -> ValidationResult:
        """
        Validate design variable bounds.

        Args:
            parameter: Parameter name
            min_value: Lower bound
            max_value: Upper bound
            current_value: Current/nominal value

        Returns:
            ValidationResult
        """
        if min_value >= max_value:
            return ValidationResult(
                valid=False,
                message=f"Invalid bounds for {parameter}: min ({min_value}) >= max ({max_value})",
                severity="error",
                field=parameter
            )

        # Check bounds width
        if min_value > 0:
            ratio = max_value / min_value
            if ratio > 10:
                return ValidationResult(
                    valid=True,
                    message=f"Wide bounds for {parameter}: ratio {ratio:.1f}x may slow convergence",
                    severity="warning",
                    suggestion=f"Consider narrowing to {min_value:.2f} - {min_value * 5:.2f}",
                    field=parameter
                )

        # Check if current value is within bounds
        if current_value is not None:
            if current_value < min_value or current_value > max_value:
                return ValidationResult(
                    valid=True,
                    message=f"Current value ({current_value}) for {parameter} is outside bounds [{min_value}, {max_value}]",
                    severity="warning",
                    suggestion="Adjust bounds to include current value or update nominal design",
                    field=parameter
                )

        return ValidationResult(
            valid=True,
            message=f"Bounds for {parameter} are valid",
            severity="ok",
            field=parameter
        )

    def suggest_bounds(
        self,
        parameter: str,
        current_value: float,
        context: Optional[Dict[str, Any]] = None
    ) -> Tuple[float, float]:
        """
        Suggest reasonable bounds for a design variable.

        Args:
            parameter: Parameter name
            current_value: Current value
            context: Optional context (material, application, etc.)

        Returns:
            Tuple of (suggested_min, suggested_max)
        """
        # Default: +/- 50% of current value
        if current_value > 0:
            suggested_min = current_value * 0.5
            suggested_max = current_value * 1.5
        elif current_value < 0:
            suggested_min = current_value * 1.5
            suggested_max = current_value * 0.5
        else:
            suggested_min = -1.0
            suggested_max = 1.0

        # Adjust based on parameter name heuristics
        name_lower = parameter.lower()

        if "thickness" in name_lower:
            # Thickness should stay positive with reasonable manufacturing limits
            suggested_min = max(0.5, current_value * 0.3)  # Min 0.5mm
            suggested_max = current_value * 2.0

        elif "radius" in name_lower or "fillet" in name_lower:
            # Radii should stay positive
            suggested_min = max(0.1, current_value * 0.2)
            suggested_max = current_value * 3.0

        elif "angle" in name_lower:
            # Angles often have natural limits
            suggested_min = max(-90, current_value - 30)
            suggested_max = min(90, current_value + 30)

        return (round(suggested_min, 3), round(suggested_max, 3))

    def detect_anti_patterns(
        self,
        state: "InterviewState",
        introspection: Optional[Dict[str, Any]] = None
    ) -> List[AntiPattern]:
        """
        Detect common optimization anti-patterns.

        Args:
            state: Current interview state
            introspection: Optional introspection results

        Returns:
            List of detected anti-patterns
        """
        return self.anti_patterns.check_all(state, introspection or {})

    def validate_all(
        self,
        state: "InterviewState",
        introspection: Optional[Dict[str, Any]] = None
    ) -> List[ValidationResult]:
        """
        Run all validations on interview state.

        Args:
            state: Current interview state
            introspection: Optional introspection results

        Returns:
            List of all validation results
        """
        results = []
        answers = state.answers
        intro = introspection or {}

        # Validate constraints
        if "max_stress" in answers.get("constraints", {}):
            material = intro.get("material", {}).get("name")
            result = self.validate_constraint(
                "stress",
                answers["constraints"]["max_stress"],
                material=material,
                baseline=intro.get("baseline_stress")
            )
            results.append(result)

        if "max_displacement" in answers.get("constraints", {}):
            result = self.validate_constraint(
                "displacement",
                answers["constraints"]["max_displacement"],
                baseline=intro.get("baseline_displacement")
            )
            results.append(result)

        if "min_frequency" in answers.get("constraints", {}):
            result = self.validate_constraint(
                "frequency",
                answers["constraints"]["min_frequency"],
                baseline=intro.get("baseline_frequency")
            )
            results.append(result)

        # Validate design variable bounds
        for dv in answers.get("design_variables", []):
            if isinstance(dv, dict):
                result = self.validate_bounds(
                    dv.get("parameter", "unknown"),
                    dv.get("min_value", 0),
                    dv.get("max_value", 1),
                    dv.get("current_value")
                )
                results.append(result)

        # Check anti-patterns
        anti_patterns = self.detect_anti_patterns(state, intro)
        for ap in anti_patterns:
            results.append(ValidationResult(
                valid=ap.severity != "error",
                message=f"[{ap.name}] {ap.description}",
                severity=ap.severity,
                suggestion=ap.fix_suggestion
            ))

        return results

    def has_blocking_issues(
        self,
        state: "InterviewState",
        introspection: Optional[Dict[str, Any]] = None
    ) -> Tuple[bool, List[str]]:
        """
        Check if there are any blocking issues.

        Returns:
            Tuple of (has_blocking, list_of_blocking_messages)
        """
        results = self.validate_all(state, introspection)
        blocking = [r.message for r in results if r.is_blocking()]
        return len(blocking) > 0, blocking


# Import for type hints
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from .interview_state import InterviewState
