"""AI Plan Generator (P3-M3).

Converts natural language requirements into structured simulation plans
using Kimi k3 model, with L0 pre-screening validation.
"""
import json
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple

from ..config import PROMPTS_DIR
from ..services.ai_client import get_kimi_client
from ..services.l0_prescreening import L0PreScreeningEngine


class AIPlanGenerator:
    """Generate simulation plans from natural language using AI."""

    def __init__(self, l0_engine: Optional[L0PreScreeningEngine] = None):
        self.ai_client = get_kimi_client()
        self.l0_engine = l0_engine or L0PreScreeningEngine()
        self._prompt_template = None

    def _load_prompt(self) -> str:
        """Load plan generation prompt template."""
        if self._prompt_template is None:
            prompt_path = PROMPTS_DIR / "plan_generation" / "generate.txt"
            if prompt_path.exists():
                with open(prompt_path, "r", encoding="utf-8") as f:
                    self._prompt_template = f.read()
            else:
                self._prompt_template = self._default_prompt()
        return self._prompt_template

    def _default_prompt(self) -> str:
        """Fallback default prompt."""
        return """你是轴向磁通电机仿真方案专家。将用户需求转化为JSON格式的仿真方案，包含plan_name、topology、boundary_conditions、scan_variables、search_strategy、acceptance_criteria、reasoning。输出纯JSON。"""

    def generate(
        self,
        user_requirement: str,
        project_context: Optional[Dict[str, Any]] = None,
        existing_experience: Optional[List[Dict[str, Any]]] = None,
    ) -> Dict[str, Any]:
        """Generate a simulation plan from natural language.

        Args:
            user_requirement: Natural language description of the simulation need.
            project_context: Optional project context (topology, existing boundary conditions).
            existing_experience: Optional similar experience cases for reference.

        Returns:
            Dict with generated plan, validation results, and AI reasoning.
        """
        if not self.ai_client.is_configured:
            raise RuntimeError("KIMI_API_KEY is not configured")

        # Build context message
        context_parts = []
        if project_context:
            context_parts.append(f"项目上下文：{json.dumps(project_context, ensure_ascii=False)}")
        if existing_experience:
            context_parts.append(f"参考经验案例（{len(existing_experience)}个）：{json.dumps(existing_experience[:3], ensure_ascii=False)}")

        user_message = user_requirement
        if context_parts:
            user_message += "\n\n" + "\n".join(context_parts)

        # Call AI
        system_prompt = self._load_prompt()
        result = self.ai_client.chat_json(
            messages=[{"role": "user", "content": user_message}],
            system_prompt=system_prompt,
            max_tokens=4000,
        )

        # Parse generated plan
        plan = result.get("parsed_json", {})
        if not plan:
            # Try to extract and repair JSON from raw content
            raw = result.get("raw_content", result.get("content", ""))
            plan = self._try_repair_json(raw)
            if not plan:
                plan = {"error": "Failed to parse AI response", "raw_content": raw[:1000]}

        # Validate with L0 pre-screening
        validation = self._validate_plan(plan)

        return {
            "plan": plan,
            "validation": validation,
            "ai_reasoning": plan.get("reasoning", ""),
            "usage": result.get("usage", {}),
            "raw_content": result.get("content", ""),
        }

    def _try_repair_json(self, raw: str) -> Dict[str, Any]:
        """Try to extract and repair JSON from potentially truncated AI output.

        Handles:
        - Markdown code fences
        - Truncated JSON (missing closing braces)
        - Extra text before/after JSON
        """
        if not raw:
            return {}

        text = raw.strip()

        # Remove markdown code fences
        if text.startswith("```"):
            lines = text.split("\n")
            if lines[0].startswith("```"):
                lines = lines[1:]
            if lines and lines[-1].strip() == "```":
                lines = lines[:-1]
            text = "\n".join(lines).strip()

        # Find first { and last }
        first_brace = text.find("{")
        last_brace = text.rfind("}")

        if first_brace == -1:
            return {}

        if last_brace == -1 or last_brace < first_brace:
            # JSON is truncated - try to close it
            json_str = text[first_brace:]
            # Count open braces and close them
            open_braces = json_str.count("{") - json_str.count("}")
            open_brackets = json_str.count("[") - json_str.count("]")
            # Remove trailing incomplete content
            last_comma = max(json_str.rfind(","), json_str.rfind(":"))
            if last_comma > len(json_str) * 0.8:
                json_str = json_str[:last_comma]
            # Close brackets and braces
            json_str += "]" * max(0, open_brackets)
            json_str += "}" * max(0, open_braces)
        else:
            json_str = text[first_brace:last_brace + 1]

        try:
            return json.loads(json_str)
        except json.JSONDecodeError:
            # Try one more repair: remove trailing comma before closing
            try:
                repaired = json_str.rstrip()
                while repaired and repaired[-1] in ", \n\t":
                    repaired = repaired[:-1]
                # Re-count and close
                open_braces = repaired.count("{") - repaired.count("}")
                open_brackets = repaired.count("[") - repaired.count("]")
                repaired += "]" * max(0, open_brackets)
                repaired += "}" * max(0, open_braces)
                return json.loads(repaired)
            except (json.JSONDecodeError, Exception):
                return {}

    def _validate_plan(self, plan: Dict[str, Any]) -> Dict[str, Any]:
        """Validate generated plan with L0 pre-screening.

        Checks:
        1. Boundary conditions feasibility
        2. Scan variable ranges within engineering limits
        3. Sample point count estimation
        4. Required fields presence
        """
        issues = []
        warnings = []
        checks = []

        # Check required fields
        required_fields = ["plan_name", "topology", "scan_variables"]
        for field in required_fields:
            if field not in plan:
                issues.append(f"Missing required field: {field}")

        # Validate topology
        topology = plan.get("topology", "")
        if topology and topology not in ("SSSR", "DRSS", "SDSR"):
            warnings.append(f"Unusual topology: {topology}")

        # Validate boundary conditions with L0
        bc = plan.get("boundary_conditions", {})
        if bc:
            l0_report = self.l0_engine.evaluate(bc)
            checks.append({
                "name": "boundary_conditions_l0",
                "feasible": l0_report.feasible,
                "passed": l0_report.passed_checks,
                "total": l0_report.total_checks,
                "failed_items": [r.name for r in l0_report.results if not r.passed],
            })
            if not l0_report.feasible:
                issues.append(f"Boundary conditions infeasible: {l0_report.failed_checks} checks failed")

        # Validate scan variables
        variables = plan.get("scan_variables", [])
        if not isinstance(variables, list):
            variables = []
        total_points = 1
        normalized_vars = []
        for var in variables:
            if isinstance(var, str):
                var = {"name": var, "min_value": None, "max_value": None}
            if not isinstance(var, dict):
                continue
            normalized_vars.append(var)
            name = var.get("name", "unknown")
            min_v = var.get("min_value")
            max_v = var.get("max_value")
            step = var.get("step")

            if min_v is None or max_v is None:
                issues.append(f"Variable {name}: missing min/max values")
                continue

            if min_v >= max_v:
                issues.append(f"Variable {name}: min ({min_v}) >= max ({max_v})")

            if step and step > 0:
                n_points = int((max_v - min_v) / step) + 1
                total_points *= n_points
                if n_points > 50:
                    warnings.append(f"Variable {name}: {n_points} levels may be too many")
        plan["scan_variables"] = normalized_vars

        checks.append({
            "name": "scan_variables",
            "count": len(variables),
            "estimated_full_factorial_points": total_points,
            "adaptive_search_recommended": total_points > 100,
        })

        if total_points > 500:
            warnings.append(f"Full factorial would require {total_points} points - strongly recommend adaptive search")

        # Validate search strategy
        search = plan.get("search_strategy", {})
        if search:
            max_calls = search.get("max_solver_calls", 80)
            if max_calls < total_points and not search.get("method", "").startswith(("constrained", "active")):
                warnings.append(f"Budget ({max_calls}) < full factorial ({total_points}) but method is not adaptive")

        overall_valid = len(issues) == 0
        return {
            "valid": overall_valid,
            "issues": issues,
            "warnings": warnings,
            "checks": checks,
        }

    def refine_plan(
        self,
        original_plan: Dict[str, Any],
        user_feedback: str,
    ) -> Dict[str, Any]:
        """Refine an existing plan based on user feedback.

        Args:
            original_plan: The previously generated plan.
            user_feedback: Natural language feedback for refinement.

        Returns:
            Refined plan with validation.
        """
        if not self.ai_client.is_configured:
            raise RuntimeError("KIMI_API_KEY is not configured")

        system_prompt = self._load_prompt() + "\n\n你正在优化一个已有的仿真方案。根据用户反馈调整方案，保持其他部分不变。"

        user_message = f"原有方案：\n{json.dumps(original_plan, ensure_ascii=False, indent=2)}\n\n用户反馈：{user_feedback}\n\n请输出优化后的完整方案JSON。"

        result = self.ai_client.chat_json(
            messages=[{"role": "user", "content": user_message}],
            system_prompt=system_prompt,
            max_tokens=4000,
        )

        refined_plan = result.get("parsed_json", {})
        if not refined_plan:
            raw = result.get("raw_content", result.get("content", ""))
            refined_plan = self._try_repair_json(raw)
            if not refined_plan:
                refined_plan = {"error": "Failed to parse AI response", "raw_content": raw[:1000]}

        validation = self._validate_plan(refined_plan)

        return {
            "plan": refined_plan,
            "validation": validation,
            "ai_reasoning": refined_plan.get("reasoning", ""),
            "usage": result.get("usage", {}),
        }


# Global singleton
_generator: Optional[AIPlanGenerator] = None


def get_plan_generator() -> AIPlanGenerator:
    """Get or create global AIPlanGenerator singleton."""
    global _generator
    if _generator is None:
        _generator = AIPlanGenerator()
    return _generator
