from __future__ import annotations
from dataclasses import dataclass, field, asdict, KW_ONLY
from typing import Optional

@dataclass
class Soil:
    """
    Refactored Soil class with better validation and dictionary handling.
    """
    name: str
    unit_weight: float
    unit_weight_sat: Optional[float] = None
    description: str = ""
    
    _: KW_ONLY
    su: Optional[float] = None
    phi: Optional[float] = None
    c: Optional[float] = None
    spt_n: Optional[int] = None
    void_ratio: Optional[float] = None
    specific_gravity: float = 2.7
    extra_params: dict = field(default_factory=dict)

    def __post_init__(self) -> None:
        self._validate_physical_bounds()
        # Ensure unit_weight_sat is never None for calculation safety
        if self.unit_weight_sat is None:
            self.unit_weight_sat = self.unit_weight

    def _validate_physical_bounds(self) -> None:
        """Centralized validation logic."""
        checks = [
            (self.unit_weight <= 0, f"unit_weight must be > 0, got {self.unit_weight}"),
            (self.specific_gravity <= 0, f"specific_gravity must be > 0, got {self.specific_gravity}"),
            (self.unit_weight_sat is not None and self.unit_weight_sat < self.unit_weight, 
             f"unit_weight_sat ({self.unit_weight_sat}) cannot be less than unit_weight ({self.unit_weight})")
        ]
        
        for condition, msg in checks:
            if condition:
                raise ValueError(msg)
        
        # Geotechnical specific bounds
        if self.phi is not None and not (0 < self.phi < 90):
            raise ValueError(f"phi must be between 0 and 90 degrees, got {self.phi}")
        if self.void_ratio is not None and self.void_ratio <= 0:
            raise ValueError(f"void_ratio must be > 0, got {self.void_ratio}")

    @property
    def porosity(self) -> Optional[float]:
        """n = e / (1 + e)"""
        if self.void_ratio is None:
            return None
        return self.void_ratio / (1.0 + self.void_ratio)

    def to_dict(self) -> dict:
        """
        Uses dataclass utilities to ensure all fields (including specific_gravity 
        and void_ratio) are captured, then merges extra_params.
        """
        data = asdict(self)
        extras = data.pop("extra_params", {})
        return {**data, **extras}

    def __repr__(self) -> str:
        return (f"Soil(name={self.name!r}, γ={self.unit_weight:.2f}, "
                f"γ_sat={self.unit_weight_sat:.2f}, desc={self.description!r})")