"""L0 Analytic Pre-screening Engine (P3-M2).

Per third-party review: L0 analytic + rule-based pre-filtering
to exclude obviously infeasible regions before any simulation.

Covers:
- Geometric constraints (inner/outer diameter, airgap, axial length)
- Electrical constraints (current density, voltage, magnetic loading)
- Thermal constraints (temperature rise, cooling capacity)
- Manufacturing constraints (PCB line width/spacing, copper thickness, tolerances)
"""
import math
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, Any


@dataclass
class ConstraintResult:
    """Result of a single constraint check."""
    name: str
    category: str  # geometric / electrical / thermal / manufacturing
    passed: bool
    value: Optional[float] = None
    limit: Optional[float] = None
    margin: Optional[float] = None  # percentage margin (positive = safe)
    message: str = ""


@dataclass
class FeasibilityReport:
    """Complete L0 feasibility report for a parameter set."""
    feasible: bool
    total_checks: int
    passed_checks: int
    failed_checks: int
    results: List[ConstraintResult] = field(default_factory=list)
    risk_items: List[str] = field(default_factory=list)

    @property
    def pass_rate(self) -> float:
        return self.passed_checks / self.total_checks if self.total_checks > 0 else 0.0

    def to_dict(self) -> Dict[str, Any]:
        return {
            "feasible": self.feasible,
            "total_checks": self.total_checks,
            "passed_checks": self.passed_checks,
            "failed_checks": self.failed_checks,
            "pass_rate": round(self.pass_rate, 4),
            "results": [
                {
                    "name": r.name,
                    "category": r.category,
                    "passed": r.passed,
                    "value": r.value,
                    "limit": r.limit,
                    "margin_pct": round(r.margin, 2) if r.margin is not None else None,
                    "message": r.message,
                }
                for r in self.results
            ],
            "risk_items": self.risk_items,
        }


class L0PreScreeningEngine:
    """L0 analytic pre-screening engine.

    Uses first-order physics and engineering rules to quickly
    reject infeasible parameter combinations without simulation.
    """

    # Default engineering limits (can be overridden per project)
    DEFAULT_LIMITS = {
        # Geometric
        "min_outer_diameter_mm": 20.0,
        "max_outer_diameter_mm": 500.0,
        "min_inner_diameter_mm": 5.0,
        "min_diameter_ratio": 0.2,  # inner/outer
        "max_diameter_ratio": 0.8,
        "min_airgap_mm": 0.3,
        "max_airgap_mm": 5.0,
        "min_magnet_thickness_mm": 1.0,
        "max_magnet_thickness_mm": 20.0,
        # Electrical
        "max_current_density_amm2": 15.0,  # A/mm^2 (natural convection)
        "max_current_density_forced_amm2": 25.0,  # A/mm^2 (forced cooling)
        "min_slot_fill_factor": 0.3,
        "max_slot_fill_factor": 0.75,
        "max_magnetic_loading_t": 1.8,  # Tesla (avoid saturation)
        # Thermal
        "max_temperature_rise_c": 80.0,  # K (above ambient)
        "max_winding_temp_c": 150.0,  # Class F
        "max_magnet_temp_c": 120.0,  # NdFeB N42SH
        # Manufacturing (PCB)
        "min_pcb_line_width_mm": 0.1,
        "min_pcb_line_spacing_mm": 0.1,
        "min_pcb_copper_thickness_oz": 0.5,
        "max_pcb_copper_thickness_oz": 6.0,
        "min_via_diameter_mm": 0.2,
        "max_pcb_layers": 20,
    }

    def __init__(self, limits: Optional[Dict[str, float]] = None):
        self.limits = dict(self.DEFAULT_LIMITS)
        if limits:
            self.limits.update(limits)

    def check_geometric(self, params: Dict[str, Any]) -> List[ConstraintResult]:
        """Check geometric feasibility constraints."""
        results = []
        L = self.limits

        outer_d = params.get("outer_diameter_mm")
        inner_d = params.get("inner_diameter_mm")
        airgap = params.get("airgap_mm")
        magnet_thickness = params.get("magnet_thickness_mm")

        # Outer diameter range
        if outer_d is not None:
            passed = L["min_outer_diameter_mm"] <= outer_d <= L["max_outer_diameter_mm"]
            results.append(ConstraintResult(
                name="outer_diameter_range",
                category="geometric",
                passed=passed,
                value=outer_d,
                limit=f"{L['min_outer_diameter_mm']}-{L['max_outer_diameter_mm']}",
                message=f"Outer diameter {outer_d}mm {'within' if passed else 'outside'} valid range",
            ))

        # Inner diameter and ratio
        if outer_d is not None and inner_d is not None:
            ratio = inner_d / outer_d if outer_d > 0 else 0
            passed = (L["min_inner_diameter_mm"] <= inner_d and
                      L["min_diameter_ratio"] <= ratio <= L["max_diameter_ratio"])
            margin = (ratio - L["min_diameter_ratio"]) / L["min_diameter_ratio"] * 100 if ratio >= L["min_diameter_ratio"] else None
            results.append(ConstraintResult(
                name="inner_diameter_ratio",
                category="geometric",
                passed=passed,
                value=round(ratio, 3),
                limit=f"{L['min_diameter_ratio']}-{L['max_diameter_ratio']}",
                margin=margin,
                message=f"Inner/outer diameter ratio {ratio:.3f} {'valid' if passed else 'invalid'}",
            ))

        # Airgap range
        if airgap is not None:
            passed = L["min_airgap_mm"] <= airgap <= L["max_airgap_mm"]
            results.append(ConstraintResult(
                name="airgap_range",
                category="geometric",
                passed=passed,
                value=airgap,
                limit=f"{L['min_airgap_mm']}-{L['max_airgap_mm']}",
                message=f"Airgap {airgap}mm {'within' if passed else 'outside'} valid range",
            ))

        # Magnet thickness
        if magnet_thickness is not None:
            passed = L["min_magnet_thickness_mm"] <= magnet_thickness <= L["max_magnet_thickness_mm"]
            results.append(ConstraintResult(
                name="magnet_thickness_range",
                category="geometric",
                passed=passed,
                value=magnet_thickness,
                limit=f"{L['min_magnet_thickness_mm']}-{L['max_magnet_thickness_mm']}",
                message=f"Magnet thickness {magnet_thickness}mm {'within' if passed else 'outside'} valid range",
            ))

        # Airgap vs magnet thickness ratio (engineering rule)
        if airgap is not None and magnet_thickness is not None:
            ratio = airgap / magnet_thickness if magnet_thickness > 0 else 0
            passed = 0.05 <= ratio <= 0.5  # typical: airgap 5-50% of magnet thickness
            results.append(ConstraintResult(
                name="airgap_magnet_ratio",
                category="geometric",
                passed=passed,
                value=round(ratio, 3),
                limit="0.05-0.5",
                message=f"Airgap/magnet thickness ratio {ratio:.3f} {'reasonable' if passed else 'unusual'}",
            ))

        return results

    def check_electrical(self, params: Dict[str, Any]) -> List[ConstraintResult]:
        """Check electrical feasibility constraints."""
        results = []
        L = self.limits

        current_density = params.get("current_density_amm2")
        rms_current = params.get("current_a") or params.get("rms_current_a")
        conductor_area = params.get("conductor_area_mm2")
        slot_fill_factor = params.get("slot_fill_factor")
        magnetic_loading = params.get("magnetic_loading_t") or params.get("airgap_flux_density_t")
        forced_cooling = params.get("forced_cooling", False)

        # Current density (derived or direct)
        if current_density is None and rms_current is not None and conductor_area is not None and conductor_area > 0:
            current_density = rms_current / conductor_area

        if current_density is not None:
            limit = L["max_current_density_forced_amm2"] if forced_cooling else L["max_current_density_amm2"]
            passed = current_density <= limit
            margin = (limit - current_density) / limit * 100 if current_density > 0 else None
            results.append(ConstraintResult(
                name="current_density",
                category="electrical",
                passed=passed,
                value=round(current_density, 2),
                limit=limit,
                margin=margin,
                message=f"Current density {current_density:.1f} A/mm^2 {'within' if passed else 'exceeds'} {limit} A/mm^2 limit ({'forced' if forced_cooling else 'natural'} cooling)",
            ))

        # Slot fill factor
        if slot_fill_factor is not None:
            passed = L["min_slot_fill_factor"] <= slot_fill_factor <= L["max_slot_fill_factor"]
            results.append(ConstraintResult(
                name="slot_fill_factor",
                category="electrical",
                passed=passed,
                value=slot_fill_factor,
                limit=f"{L['min_slot_fill_factor']}-{L['max_slot_fill_factor']}",
                message=f"Slot fill factor {slot_fill_factor:.2f} {'within' if passed else 'outside'} valid range",
            ))

        # Magnetic loading (saturation check)
        if magnetic_loading is not None:
            passed = magnetic_loading <= L["max_magnetic_loading_t"]
            margin = (L["max_magnetic_loading_t"] - magnetic_loading) / L["max_magnetic_loading_t"] * 100
            results.append(ConstraintResult(
                name="magnetic_loading",
                category="electrical",
                passed=passed,
                value=round(magnetic_loading, 3),
                limit=L["max_magnetic_loading_t"],
                margin=margin,
                message=f"Magnetic loading {magnetic_loading:.2f}T {'below' if passed else 'exceeds'} {L['max_magnetic_loading_t']}T saturation limit",
            ))

        return results

    def check_thermal(self, params: Dict[str, Any]) -> List[ConstraintResult]:
        """Check thermal feasibility constraints (first-order estimates)."""
        results = []
        L = self.limits

        ambient_temp = params.get("ambient_temp_c", 25.0)
        winding_temp = params.get("winding_temp_c")
        magnet_temp = params.get("magnet_temp_c")
        total_losses_w = params.get("total_losses_w")
        cooling_area_mm2 = params.get("cooling_area_mm2")

        # Winding temperature limit
        if winding_temp is not None:
            passed = winding_temp <= L["max_winding_temp_c"]
            margin = (L["max_winding_temp_c"] - winding_temp) / L["max_winding_temp_c"] * 100
            results.append(ConstraintResult(
                name="winding_temperature",
                category="thermal",
                passed=passed,
                value=winding_temp,
                limit=L["max_winding_temp_c"],
                margin=margin,
                message=f"Winding temperature {winding_temp}C {'below' if passed else 'exceeds'} {L['max_winding_temp_c']}C limit (Class F)",
            ))

        # Magnet temperature limit
        if magnet_temp is not None:
            passed = magnet_temp <= L["max_magnet_temp_c"]
            margin = (L["max_magnet_temp_c"] - magnet_temp) / L["max_magnet_temp_c"] * 100
            results.append(ConstraintResult(
                name="magnet_temperature",
                category="thermal",
                passed=passed,
                value=magnet_temp,
                limit=L["max_magnet_temp_c"],
                margin=margin,
                message=f"Magnet temperature {magnet_temp}C {'below' if passed else 'exceeds'} {L['max_magnet_temp_c']}C limit (NdFeB N42SH)",
            ))

        # First-order thermal estimate: losses vs cooling capacity
        if total_losses_w is not None and cooling_area_mm2 is not None and cooling_area_mm2 > 0:
            # Rough heat transfer coefficient: 10 W/m^2K (natural), 50 W/m^2K (forced)
            forced = params.get("forced_cooling", False)
            h = 50.0 if forced else 10.0  # W/m^2K
            cooling_area_m2 = cooling_area_mm2 / 1e6
            temp_rise = total_losses_w / (h * cooling_area_m2) if cooling_area_m2 > 0 else float('inf')
            passed = temp_rise <= L["max_temperature_rise_c"]
            results.append(ConstraintResult(
                name="thermal_estimate",
                category="thermal",
                passed=passed,
                value=round(temp_rise, 1),
                limit=L["max_temperature_rise_c"],
                message=f"Estimated temperature rise {temp_rise:.1f}K {'within' if passed else 'exceeds'} {L['max_temperature_rise_c']}K limit (rough estimate, needs L2 verification)",
            ))

        return results

    def check_manufacturing(self, params: Dict[str, Any]) -> List[ConstraintResult]:
        """Check PCB manufacturing constraints."""
        results = []
        L = self.limits

        pcb_line_width = params.get("pcb_line_width_mm")
        pcb_line_spacing = params.get("pcb_line_spacing_mm")
        pcb_copper_thickness_oz = params.get("pcb_copper_thickness_oz")
        pcb_layers = params.get("pcb_layers")
        via_diameter = params.get("via_diameter_mm")

        if pcb_line_width is not None:
            passed = pcb_line_width >= L["min_pcb_line_width_mm"]
            results.append(ConstraintResult(
                name="pcb_line_width",
                category="manufacturing",
                passed=passed,
                value=pcb_line_width,
                limit=L["min_pcb_line_width_mm"],
                message=f"PCB line width {pcb_line_width}mm {'meets' if passed else 'below'} {L['min_pcb_line_width_mm']}mm minimum",
            ))

        if pcb_line_spacing is not None:
            passed = pcb_line_spacing >= L["min_pcb_line_spacing_mm"]
            results.append(ConstraintResult(
                name="pcb_line_spacing",
                category="manufacturing",
                passed=passed,
                value=pcb_line_spacing,
                limit=L["min_pcb_line_spacing_mm"],
                message=f"PCB line spacing {pcb_line_spacing}mm {'meets' if passed else 'below'} {L['min_pcb_line_spacing_mm']}mm minimum",
            ))

        if pcb_copper_thickness_oz is not None:
            passed = L["min_pcb_copper_thickness_oz"] <= pcb_copper_thickness_oz <= L["max_pcb_copper_thickness_oz"]
            results.append(ConstraintResult(
                name="pcb_copper_thickness",
                category="manufacturing",
                passed=passed,
                value=pcb_copper_thickness_oz,
                limit=f"{L['min_pcb_copper_thickness_oz']}-{L['max_pcb_copper_thickness_oz']}",
                message=f"PCB copper thickness {pcb_copper_thickness_oz}oz {'within' if passed else 'outside'} standard range",
            ))

        if pcb_layers is not None:
            passed = pcb_layers <= L["max_pcb_layers"]
            results.append(ConstraintResult(
                name="pcb_layer_count",
                category="manufacturing",
                passed=passed,
                value=pcb_layers,
                limit=L["max_pcb_layers"],
                message=f"PCB layer count {pcb_layers} {'within' if passed else 'exceeds'} {L['max_pcb_layers']} layer limit",
            ))

        return results

    def evaluate(self, params: Dict[str, Any]) -> FeasibilityReport:
        """Run full L0 pre-screening on a parameter set.

        Args:
            params: Dictionary of parameter values (diameters, airgap, currents, etc.)

        Returns:
            FeasibilityReport with all check results and overall feasibility.
        """
        all_results = []
        all_results.extend(self.check_geometric(params))
        all_results.extend(self.check_electrical(params))
        all_results.extend(self.check_thermal(params))
        all_results.extend(self.check_manufacturing(params))

        passed = sum(1 for r in all_results if r.passed)
        failed = len(all_results) - passed
        feasible = failed == 0

        # Collect risk items (failed or low-margin checks)
        risk_items = []
        for r in all_results:
            if not r.passed:
                risk_items.append(f"[FAIL] {r.name}: {r.message}")
            elif r.margin is not None and r.margin < 10:
                risk_items.append(f"[RISK] {r.name}: margin only {r.margin:.1f}%")

        return FeasibilityReport(
            feasible=feasible,
            total_checks=len(all_results),
            passed_checks=passed,
            failed_checks=failed,
            results=all_results,
            risk_items=risk_items,
        )

    def is_feasible(self, params: Dict[str, Any]) -> bool:
        """Quick feasibility check (boolean only)."""
        return self.evaluate(params).feasible

    def filter_feasible(self, param_sets: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
        """Filter a list of parameter sets into feasible and infeasible.

        Returns:
            Tuple of (feasible_sets, infeasible_sets)
        """
        feasible = []
        infeasible = []
        for params in param_sets:
            if self.is_feasible(params):
                feasible.append(params)
            else:
                infeasible.append(params)
        return feasible, infeasible
