"""
Interview Intelligence

Smart features for the interview process:
- ExtractorMapper: Maps goals to appropriate extractors
- InterviewIntelligence: Auto-detection, inference, complexity determination
"""

from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Any, Optional, Literal, Tuple
import re


@dataclass
class ExtractorSelection:
    """Result of mapping a goal to an extractor."""
    extractor_id: str
    extractor_name: str
    goal_type: str  # minimize, maximize, target
    params: Dict[str, Any] = field(default_factory=dict)
    fallback: Optional[str] = None
    confidence: float = 1.0
    notes: Optional[str] = None


class ExtractorMapper:
    """
    Maps physics goals to appropriate extractors.

    Uses the Atomizer extractor library (SYS_12) to select
    the right extractor for each objective or constraint.
    """

    # Goal to extractor mapping
    GOAL_MAP = {
        # Mass objectives
        "minimize_mass": ExtractorSelection(
            extractor_id="E4",
            extractor_name="BDF Mass Extraction",
            goal_type="minimize",
            fallback="E5",
            notes="Uses BDF parsing for accurate mass. Falls back to NX expression."
        ),
        "minimize_weight": ExtractorSelection(
            extractor_id="E4",
            extractor_name="BDF Mass Extraction",
            goal_type="minimize",
            fallback="E5"
        ),

        # Displacement/stiffness objectives
        "minimize_displacement": ExtractorSelection(
            extractor_id="E1",
            extractor_name="Displacement Extraction",
            goal_type="minimize",
            params={"component": "magnitude", "node_id": "auto"},
            notes="Extracts displacement magnitude. Node ID auto-detected from max."
        ),
        "maximize_stiffness": ExtractorSelection(
            extractor_id="E1",
            extractor_name="Displacement Extraction",
            goal_type="minimize",  # Stiffness = 1/displacement
            params={"component": "magnitude", "node_id": "auto"},
            notes="Stiffness maximization = displacement minimization"
        ),

        # Frequency objectives
        "maximize_frequency": ExtractorSelection(
            extractor_id="E2",
            extractor_name="Frequency Extraction",
            goal_type="maximize",
            params={"mode_number": 1},
            notes="First natural frequency. Mode number adjustable."
        ),
        "target_frequency": ExtractorSelection(
            extractor_id="E2",
            extractor_name="Frequency Extraction",
            goal_type="target",
            params={"mode_number": 1, "target": None},
            notes="Target a specific frequency value."
        ),

        # Stress objectives
        "minimize_stress": ExtractorSelection(
            extractor_id="E3",
            extractor_name="Solid Stress Extraction",
            goal_type="minimize",
            params={"element_type": "auto", "stress_type": "von_mises"},
            notes="Von Mises stress. Element type auto-detected."
        ),

        # Optical objectives
        "minimize_wavefront_error": ExtractorSelection(
            extractor_id="E8",
            extractor_name="Zernike Wavefront Fitting",
            goal_type="minimize",
            params={"n_terms": 15, "radius": "auto"},
            notes="Fits surface to Zernike polynomials. Optical applications."
        ),

        # Custom
        "custom": ExtractorSelection(
            extractor_id="custom",
            extractor_name="Custom Extractor",
            goal_type="custom",
            confidence=0.5,
            notes="User will define custom extraction logic."
        ),
    }

    # Constraint type to extractor mapping
    CONSTRAINT_MAP = {
        "stress": ExtractorSelection(
            extractor_id="E3",
            extractor_name="Solid Stress Extraction",
            goal_type="max",
            params={"stress_type": "von_mises"}
        ),
        "max_stress": ExtractorSelection(
            extractor_id="E3",
            extractor_name="Solid Stress Extraction",
            goal_type="max",
            params={"stress_type": "von_mises"}
        ),
        "displacement": ExtractorSelection(
            extractor_id="E1",
            extractor_name="Displacement Extraction",
            goal_type="max",
            params={"component": "magnitude"}
        ),
        "max_displacement": ExtractorSelection(
            extractor_id="E1",
            extractor_name="Displacement Extraction",
            goal_type="max",
            params={"component": "magnitude"}
        ),
        "frequency": ExtractorSelection(
            extractor_id="E2",
            extractor_name="Frequency Extraction",
            goal_type="min",
            params={"mode_number": 1}
        ),
        "min_frequency": ExtractorSelection(
            extractor_id="E2",
            extractor_name="Frequency Extraction",
            goal_type="min",
            params={"mode_number": 1}
        ),
        "mass": ExtractorSelection(
            extractor_id="E4",
            extractor_name="BDF Mass Extraction",
            goal_type="max"
        ),
        "max_mass": ExtractorSelection(
            extractor_id="E4",
            extractor_name="BDF Mass Extraction",
            goal_type="max"
        ),
    }

    def map_goal_to_extractor(
        self,
        goal: str,
        introspection: Optional[Dict[str, Any]] = None
    ) -> ExtractorSelection:
        """
        Map a physics goal to the appropriate extractor.

        Args:
            goal: Goal identifier (e.g., "minimize_mass")
            introspection: Optional introspection results for auto-detection

        Returns:
            ExtractorSelection with extractor details
        """
        goal_lower = goal.lower().strip()

        # Direct match
        if goal_lower in self.GOAL_MAP:
            selection = self.GOAL_MAP[goal_lower]

            # Auto-detect parameters if introspection available
            if introspection:
                selection = self._refine_selection(selection, introspection)

            return selection

        # Fuzzy matching for common variations
        for key, selection in self.GOAL_MAP.items():
            if key.replace("_", " ") in goal_lower or goal_lower in key:
                return selection

        # Default to custom
        return self.GOAL_MAP["custom"]

    def map_constraint_to_extractor(
        self,
        constraint_type: str,
        introspection: Optional[Dict[str, Any]] = None
    ) -> ExtractorSelection:
        """
        Map a constraint type to the appropriate extractor.

        Args:
            constraint_type: Constraint type (e.g., "stress", "displacement")
            introspection: Optional introspection results

        Returns:
            ExtractorSelection with extractor details
        """
        type_lower = constraint_type.lower().strip()

        if type_lower in self.CONSTRAINT_MAP:
            selection = self.CONSTRAINT_MAP[type_lower]

            if introspection:
                selection = self._refine_selection(selection, introspection)

            return selection

        # Try to infer from name
        if "stress" in type_lower:
            return self.CONSTRAINT_MAP["stress"]
        if "disp" in type_lower or "deflect" in type_lower:
            return self.CONSTRAINT_MAP["displacement"]
        if "freq" in type_lower or "modal" in type_lower:
            return self.CONSTRAINT_MAP["frequency"]
        if "mass" in type_lower or "weight" in type_lower:
            return self.CONSTRAINT_MAP["mass"]

        return ExtractorSelection(
            extractor_id="custom",
            extractor_name="Custom Constraint",
            goal_type="constraint",
            confidence=0.5
        )

    def _refine_selection(
        self,
        selection: ExtractorSelection,
        introspection: Dict[str, Any]
    ) -> ExtractorSelection:
        """Refine extractor selection based on introspection."""
        import copy
        refined = copy.deepcopy(selection)

        # Auto-detect element type for stress extraction
        if refined.extractor_id == "E3" and refined.params.get("element_type") == "auto":
            element_types = introspection.get("element_types", [])
            if "solid" in element_types or any("TET" in e or "HEX" in e for e in element_types):
                refined.params["element_type"] = "solid"
            elif "shell" in element_types or any("QUAD" in e or "TRI" in e for e in element_types):
                refined.params["element_type"] = "shell"
                refined.extractor_id = "E3_shell"  # Use shell stress extractor

        # Auto-detect node for displacement
        if refined.extractor_id == "E1" and refined.params.get("node_id") == "auto":
            # Use max displacement node from baseline if available
            if "max_disp_node" in introspection:
                refined.params["node_id"] = introspection["max_disp_node"]

        return refined

    def get_extractor_summary(self, selections: List[ExtractorSelection]) -> str:
        """Generate a summary of selected extractors."""
        lines = ["**Selected Extractors:**", ""]

        for sel in selections:
            params_str = ""
            if sel.params:
                params_str = " (" + ", ".join(f"{k}={v}" for k, v in sel.params.items()) + ")"

            lines.append(f"- **{sel.extractor_id}**: {sel.extractor_name}{params_str}")
            if sel.notes:
                lines.append(f"  > {sel.notes}")

        return "\n".join(lines)


@dataclass
class StudyTypeInference:
    """Result of inferring study type."""
    study_type: str  # single_objective, multi_objective, parametric
    protocol: str  # protocol_10_single, protocol_11_multi
    confidence: float
    reasons: List[str] = field(default_factory=list)


class InterviewIntelligence:
    """
    Smart features for the interview process.

    Provides:
    - Study type inference from context
    - Auto-selection of extractors
    - History-based suggestions
    - Complexity determination
    """

    def __init__(self):
        """Initialize intelligence module."""
        self.extractor_mapper = ExtractorMapper()

    def infer_study_type(
        self,
        study_name: str,
        user_description: str,
        introspection: Optional[Dict[str, Any]] = None
    ) -> StudyTypeInference:
        """
        Infer study type from available context.

        Args:
            study_name: Study name (may contain hints)
            user_description: User's problem description
            introspection: Optional introspection results

        Returns:
            StudyTypeInference with type and protocol
        """
        reasons = []
        score_multi = 0
        score_single = 0

        text = f"{study_name} {user_description}".lower()

        # Check for multi-objective keywords
        if any(kw in text for kw in ["pareto", "trade-off", "tradeoff", "multi-objective", "multiobjective"]):
            score_multi += 2
            reasons.append("Multi-objective keywords detected")

        if any(kw in text for kw in ["versus", " vs ", "and minimize", "and maximize", "balance"]):
            score_multi += 1
            reasons.append("Conflicting goals language detected")

        # Check for single-objective keywords
        if any(kw in text for kw in ["minimize", "maximize", "reduce", "increase"]):
            # Count occurrences
            count = sum(1 for kw in ["minimize", "maximize", "reduce", "increase"] if kw in text)
            if count == 1:
                score_single += 1
                reasons.append("Single optimization goal language")
            else:
                score_multi += 1
                reasons.append("Multiple optimization verbs detected")

        # Default to single objective if no strong signals
        if score_multi > score_single:
            return StudyTypeInference(
                study_type="multi_objective",
                protocol="protocol_11_multi",
                confidence=min(1.0, 0.5 + score_multi * 0.2),
                reasons=reasons
            )
        else:
            return StudyTypeInference(
                study_type="single_objective",
                protocol="protocol_10_single",
                confidence=min(1.0, 0.5 + score_single * 0.2),
                reasons=reasons if reasons else ["Default to single-objective"]
            )

    def auto_select_extractors(
        self,
        objectives: List[Dict[str, Any]],
        constraints: List[Dict[str, Any]],
        introspection: Optional[Dict[str, Any]] = None
    ) -> Dict[str, ExtractorSelection]:
        """
        Automatically select appropriate extractors.

        Args:
            objectives: List of objective definitions
            constraints: List of constraint definitions
            introspection: Optional introspection results

        Returns:
            Dict mapping objective/constraint names to ExtractorSelection
        """
        selections = {}

        # Map objectives
        for i, obj in enumerate(objectives):
            goal = obj.get("goal", "") if isinstance(obj, dict) else str(obj)
            name = obj.get("name", f"objective_{i}") if isinstance(obj, dict) else f"objective_{i}"

            selection = self.extractor_mapper.map_goal_to_extractor(goal, introspection)
            selections[name] = selection

        # Map constraints
        for i, con in enumerate(constraints):
            con_type = con.get("type", "") if isinstance(con, dict) else str(con)
            name = con.get("name", f"constraint_{i}") if isinstance(con, dict) else f"constraint_{i}"

            selection = self.extractor_mapper.map_constraint_to_extractor(con_type, introspection)
            selections[name] = selection

        return selections

    def determine_complexity(
        self,
        state: "InterviewState",
        introspection: Optional[Dict[str, Any]] = None
    ) -> Literal["simple", "moderate", "complex"]:
        """
        Determine study complexity for adaptive questioning.

        Based on:
        - Number of objectives
        - Number of design variables
        - Analysis complexity
        - Custom components

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

        Returns:
            Complexity level
        """
        score = 0
        answers = state.answers

        # Objectives
        n_obj = len(answers.get("objectives", []))
        secondary = answers.get("objectives_secondary", [])
        if "none" not in secondary:
            n_obj += len(secondary)

        if n_obj == 1:
            score += 0
        elif n_obj == 2:
            score += 1
        else:
            score += 2

        # Design variables
        n_dvs = len(answers.get("design_variables", []))
        if n_dvs <= 3:
            score += 0
        elif n_dvs <= 6:
            score += 1
        else:
            score += 2

        # Analysis types
        analysis_types = answers.get("analysis_types", [])
        if len(analysis_types) > 2:
            score += 2
        elif len(analysis_types) > 1:
            score += 1

        if "coupled_thermal_structural" in analysis_types:
            score += 1
        if "nonlinear" in analysis_types:
            score += 1

        # Introspection complexity
        if introspection:
            if introspection.get("multiple_solutions", False):
                score += 1
            if len(introspection.get("expressions", [])) > 20:
                score += 1

        # Categorize
        if score <= 2:
            return "simple"
        elif score <= 5:
            return "moderate"
        else:
            return "complex"

    def suggest_trial_count(
        self,
        n_design_variables: int,
        n_objectives: int,
        complexity: str
    ) -> int:
        """
        Suggest appropriate number of trials.

        Args:
            n_design_variables: Number of design variables
            n_objectives: Number of objectives
            complexity: Study complexity level

        Returns:
            Suggested trial count
        """
        # Base: 15 trials per design variable
        base = n_design_variables * 15

        # Multi-objective needs more
        if n_objectives > 1:
            base = int(base * 1.5)

        # Adjust for complexity
        if complexity == "simple":
            base = max(50, base)
        elif complexity == "moderate":
            base = max(100, base)
        else:
            base = max(150, base)

        # Round to nice numbers
        if base <= 50:
            return 50
        elif base <= 75:
            return 75
        elif base <= 100:
            return 100
        elif base <= 150:
            return 150
        elif base <= 200:
            return 200
        else:
            return int((base // 100) * 100)

    def suggest_sampler(
        self,
        n_objectives: int,
        n_design_variables: int
    ) -> str:
        """
        Suggest appropriate sampler/optimizer.

        Args:
            n_objectives: Number of objectives
            n_design_variables: Number of design variables

        Returns:
            Sampler name
        """
        if n_objectives > 1:
            return "NSGA-II"  # Multi-objective
        elif n_design_variables <= 3:
            return "TPE"  # Tree-structured Parzen Estimator
        elif n_design_variables <= 10:
            return "CMA-ES"  # Covariance Matrix Adaptation
        else:
            return "TPE"  # TPE handles high dimensions well

    def analyze_design_variable_candidates(
        self,
        expressions: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Analyze expressions to find design variable candidates.

        Args:
            expressions: List of expressions from introspection

        Returns:
            Sorted list of candidates with scores
        """
        candidates = []

        # High confidence patterns
        high_patterns = [
            (r"thickness", "Thickness parameter"),
            (r"width", "Width parameter"),
            (r"height", "Height parameter"),
            (r"diameter", "Diameter parameter"),
            (r"radius", "Radius parameter"),
            (r"length", "Length parameter"),
            (r"depth", "Depth parameter"),
            (r"angle", "Angle parameter"),
            (r"fillet", "Fillet radius"),
            (r"chamfer", "Chamfer dimension"),
            (r"rib_", "Rib parameter"),
            (r"wall_", "Wall parameter"),
            (r"flange_", "Flange parameter"),
        ]

        # Medium confidence patterns
        medium_patterns = [
            (r"dim_", "Dimension parameter"),
            (r"size_", "Size parameter"),
            (r"param_", "Named parameter"),
            (r"^p\d+$", "Numbered parameter"),
            (r"var_", "Variable"),
        ]

        # Exclusion patterns
        exclude_patterns = [
            r"mesh_", r"count_", r"num_", r"material",
            r"derived_", r"calc_", r"_result$", r"_output$",
            r"^n\d+$", r"count$"
        ]

        for expr in expressions:
            name = expr.get("name", "")
            value = expr.get("value")
            formula = expr.get("formula", "")

            # Skip non-numeric
            if not isinstance(value, (int, float)):
                continue

            # Skip formulas (computed values)
            if formula and formula != str(value):
                continue

            # Check exclusions
            if any(re.search(p, name.lower()) for p in exclude_patterns):
                continue

            # Score
            score = 0
            reason = "Named expression"

            for pattern, desc in high_patterns:
                if re.search(pattern, name.lower()):
                    score = 3
                    reason = desc
                    break

            if score == 0:
                for pattern, desc in medium_patterns:
                    if re.search(pattern, name.lower()):
                        score = 2
                        reason = desc
                        break

            if score == 0 and len(name) > 2:
                score = 1

            if score > 0:
                candidates.append({
                    "name": name,
                    "value": value,
                    "score": score,
                    "reason": reason,
                    "suggested_min": round(value * 0.5, 3) if value > 0 else round(value * 1.5, 3),
                    "suggested_max": round(value * 1.5, 3) if value > 0 else round(value * 0.5, 3),
                })

        # Sort by score descending
        candidates.sort(key=lambda x: (-x["score"], x["name"]))

        return candidates


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