"""
Pipe Ratings Calculator

This module provides functionality to calculate pipe ratings from inspection data
including QSR, QMR, QOR, SPR, MPR, OPR, SPRI, MPRI, OPRI, and LOF values.

Supports all inspection types: PACP, MACP, Mainline, and Manhole inspections.
"""

import math
from collections import Counter, OrderedDict
from dataclasses import dataclass
from typing import Dict, List, Optional, Protocol, Union

from pydantic import BaseModel, Field, field_validator

from ..defects.nassco import NasscoDefects
from ..defects.wrc import WrcDefects
from ..models.adapters import InspectionUnion
from ..models.nassco.macp.annotation_fields import MACPObservation
from ..models.nassco.pacp.annotation_fields import PACPObservation
from ..models.wrc.mainline.annotation_fields import MainlineObservation
from ..models.wrc.manhole.annotation_fields import ManholeObservation
from ..utils.defect_properties import DefectType

# Type alias for all observation types
ObservationUnion = Union[PACPObservation, MACPObservation, MainlineObservation, ManholeObservation]


@dataclass
class SeverityCounts:
    """Container for defect severity counts with validation."""

    severity_1: int = 0
    severity_2: int = 0
    severity_3: int = 0
    severity_4: int = 0
    severity_5: int = 0

    def __post_init__(self):
        """Validate severity counts are non-negative."""
        for i in range(1, 6):
            if getattr(self, f"severity_{i}") < 0:
                raise ValueError(f"Severity {i} count cannot be negative")

    def __getitem__(self, key: int) -> int:
        """Allow dictionary-like access to severity counts."""
        if not 1 <= key <= 5:
            raise KeyError(f"Invalid severity level: {key}")
        return getattr(self, f"severity_{key}")

    def __setitem__(self, key: int, value: int) -> None:
        """Allow dictionary-like setting of severity counts."""
        if not 1 <= key <= 5:
            raise KeyError(f"Invalid severity level: {key}")
        if value < 0:
            raise ValueError(f"Severity count cannot be negative: {value}")
        setattr(self, f"severity_{key}", value)

    def total_count(self) -> int:
        """Get total count of all severities."""
        return sum(getattr(self, f"severity_{i}") for i in range(1, 6))

    def to_dict(self) -> Dict[int, int]:
        """Convert to dictionary format."""
        return {i: getattr(self, f"severity_{i}") for i in range(1, 6)}


class ContinuousDefect(BaseModel):
    """Model for continuous defect data with validation."""

    defect_id: str
    severity: int
    start_distance: Optional[float] = None
    end_distance: Optional[float] = None
    start_severity: Optional[int] = None
    end_severity: Optional[int] = None
    length: Optional[float] = None

    @field_validator("severity")
    @classmethod
    def validate_severity(cls, v: int) -> int:
        """Validate severity is between 1 and 5."""
        if not 1 <= v <= 5:
            raise ValueError(f"Severity must be between 1 and 5, got {v}")
        return v

    @field_validator("start_severity", "end_severity")
    @classmethod
    def validate_severity_optional(cls, v: Optional[int]) -> Optional[int]:
        """Validate optional severity values."""
        if v is not None and not 1 <= v <= 5:
            raise ValueError(f"Severity must be between 1 and 5, got {v}")
        return v

    def calculate_length(self, division_factor: float) -> None:
        """Calculate the length of the continuous defect."""
        if self.start_distance is None or self.end_distance is None:
            raise ValueError("Cannot calculate length without start and end distances")

        if self.end_distance <= self.start_distance:
            raise ValueError("End distance must be greater than start distance")

        raw_length = (self.end_distance - self.start_distance) / division_factor
        self.length = self._custom_round(raw_length)

    def _custom_round(self, number: float) -> int:
        """Custom rounding function matching legacy behavior."""
        if number - int(number) == 0.5:
            return math.ceil(number)
        return round(number)

    def validate_severity_consistency(self) -> None:
        """Validate that start and end severities match."""
        if self.start_severity != self.end_severity:
            raise ValueError(f"Severity mismatch for continuous defect {self.defect_id}: start={self.start_severity}, end={self.end_severity}")


class DefectRatings(BaseModel):
    """Model for defect ratings calculations."""

    severity_counts: SeverityCounts = Field(default_factory=SeverityCounts)
    total_count: float = 0.0
    total_points: float = 0.0

    def add_point_defect(self, severity: int, points: Optional[float] = None) -> None:
        """Add a point defect to the ratings."""
        if not 1 <= severity <= 5:
            raise ValueError(f"Invalid severity level: {severity}")

        self.severity_counts[severity] += 1
        self.total_count += 1
        self.total_points += points or severity

    def add_continuous_defect(self, severity: int, length: float) -> None:
        """Add a continuous defect to the ratings."""
        if not 1 <= severity <= 5:
            raise ValueError(f"Invalid severity level: {severity}")
        if length < 0:
            raise ValueError(f"Length must be positive, got {length}")
        if length == 0:
            # Treat zero-length continuous defects as point defects
            self.add_point_defect(severity)
            return

        self.severity_counts[severity] += length
        self.total_count += length
        self.total_points += severity * length


class PipeRatings(BaseModel):
    """Model for all pipe ratings with validation."""

    # Quality Ratings
    qsr: str = Field(default="0000", description="Quality Structural Rating")
    qmr: str = Field(default="0000", description="Quality Maintenance Rating")
    qor: str = Field(default="0000", description="Quality Overall Rating")

    # Point Ratings
    spr: float = Field(default=0.0, description="Structural Point Rating")
    mpr: float = Field(default=0.0, description="Maintenance Point Rating")
    opr: float = Field(default=0.0, description="Overall Point Rating")

    # Rating Indices
    spri: str = Field(default="0", description="Structural Point Rating Index")
    mpri: str = Field(default="0", description="Maintenance Point Rating Index")
    opri: str = Field(default="0", description="Overall Point Rating Index")

    # Likelihood of Failure
    lof: float = Field(default=0.0, description="Likelihood of Failure")

    # Defect Counts
    structural_count: int = Field(default=0, description="Count of structural defects")
    maintenance_count: int = Field(default=0, description="Count of maintenance defects")
    critical_count: int = Field(default=0, description="Count of critical defects")

    # Structural Severity Counts
    structural_severity_1: int = Field(default=0, description="Count of structural defects with severity 1")
    structural_severity_2: int = Field(default=0, description="Count of structural defects with severity 2")
    structural_severity_3: int = Field(default=0, description="Count of structural defects with severity 3")
    structural_severity_4: int = Field(default=0, description="Count of structural defects with severity 4")
    structural_severity_5: int = Field(default=0, description="Count of structural defects with severity 5")

    # Maintenance Severity Counts
    maintenance_severity_1: int = Field(default=0, description="Count of maintenance defects with severity 1")
    maintenance_severity_2: int = Field(default=0, description="Count of maintenance defects with severity 2")
    maintenance_severity_3: int = Field(default=0, description="Count of maintenance defects with severity 3")
    maintenance_severity_4: int = Field(default=0, description="Count of maintenance defects with severity 4")
    maintenance_severity_5: int = Field(default=0, description="Count of maintenance defects with severity 5")

    @field_validator("qsr", "qmr", "qor")
    @classmethod
    def validate_quality_rating(cls, v: str) -> str:
        """Validate quality rating format."""
        if not isinstance(v, str) or len(v) != 4:
            raise ValueError(f"Quality rating must be 4 characters, got {v}")
        return v

    @field_validator("spr", "mpr", "opr", "lof")
    @classmethod
    def validate_float_values(cls, v: float) -> float:
        """Validate float values are non-negative."""
        if v < 0:
            raise ValueError(f"Value cannot be negative, got {v}")
        return v

    def to_dict(self) -> Dict[str, Union[str, float, int]]:
        """Convert to dictionary format for backward compatibility."""
        return {
            "QSR": self.qsr,
            "QMR": self.qmr,
            "QOR": self.qor,
            "SPR": self.spr,
            "MPR": self.mpr,
            "OPR": self.opr,
            "SPRI": self.spri,
            "MPRI": self.mpri,
            "OPRI": self.opri,
            "LOF": self.lof,
            "structural_count": self.structural_count,
            "maintenance_count": self.maintenance_count,
            "critical_count": self.critical_count,
            "structural_severity_1": self.structural_severity_1,
            "structural_severity_2": self.structural_severity_2,
            "structural_severity_3": self.structural_severity_3,
            "structural_severity_4": self.structural_severity_4,
            "structural_severity_5": self.structural_severity_5,
            "maintenance_severity_1": self.maintenance_severity_1,
            "maintenance_severity_2": self.maintenance_severity_2,
            "maintenance_severity_3": self.maintenance_severity_3,
            "maintenance_severity_4": self.maintenance_severity_4,
            "maintenance_severity_5": self.maintenance_severity_5,
        }


class QualityRatingCalculator:
    """Calculator for quality ratings (QSR, QMR, QOR) with improved error handling."""

    @staticmethod
    def calculate_quality_rating(defect_ratings: DefectRatings) -> str:
        """Calculate quality rating from defect ratings."""
        if not isinstance(defect_ratings, DefectRatings):
            raise TypeError("defect_ratings must be a DefectRatings instance")

        quality_rating = ""

        for severity in range(5, 0, -1):  # 5 to 1
            if defect_ratings.severity_counts[severity] > 0:
                value = QualityRatingCalculator._convert_to_alphabet(defect_ratings.severity_counts[severity])
                quality_rating += str(severity) + str(value)
                if len(quality_rating) >= 4:
                    break

        # Pad with zeros to ensure 4 characters
        while len(quality_rating) < 4:
            quality_rating += "0"

        return quality_rating

    @staticmethod
    def calculate_overall_quality_rating(qsr: str, qmr: str) -> str:
        """Calculate overall quality rating (QOR) from QSR and QMR."""
        if not isinstance(qsr, str) or not isinstance(qmr, str):
            raise TypeError("QSR and QMR must be strings")

        qor_dict = {}

        # Parse QSR and QMR to get counts by grade
        for grade in range(5, 0, -1):
            qsr_count = QualityRatingCalculator._extract_count_from_quality_rating(qsr, grade)
            qmr_count = QualityRatingCalculator._extract_count_from_quality_rating(qmr, grade)

            total_count = qsr_count + qmr_count
            if total_count > 0:
                qor_dict[grade] = total_count

        # Convert counts to alphabet representation
        for grade, count in qor_dict.items():
            qor_dict[grade] = QualityRatingCalculator._convert_to_alphabet(count)

        # Sort by grade (descending) and take top 2, then sort again
        qor_items = sorted(qor_dict.items(), key=lambda x: x[0], reverse=True)[:2]
        qor_items.sort(reverse=True)

        # Build QOR string
        qor = ""
        for grade, alpha in qor_items:
            qor += str(grade) + alpha

        # Pad with zeros to ensure 4 characters
        while len(qor) < 4:
            qor += "0"

        return qor

    @staticmethod
    def overall_quick_rating(qsr: str, qmr: str) -> Dict[str, Union[str, float]]:
        """
        Calculate overall quick rating and likelihood of failure (LOF).

        This is the modern equivalent of the legacy overall_quick_rating function.

        Args:
            qsr: Quality Structural Rating
            qmr: Quality Maintenance Rating

        Returns:
            Dictionary with 'overall' rating and 'lof' (likelihood of failure)
        """
        try:
            if not qsr and not qmr:
                return {"overall": "0000", "lof": 0.0}

            # Parse QSR and QMR to get counts by grade
            qsr_counts = {}
            qmr_counts = {}

            for grade in range(5, 0, -1):
                qsr_count = QualityRatingCalculator._extract_count_from_quality_rating(qsr, grade)
                qmr_count = QualityRatingCalculator._extract_count_from_quality_rating(qmr, grade)

                if qsr_count > 0:
                    qsr_counts[grade] = qsr_count
                if qmr_count > 0:
                    qmr_counts[grade] = qmr_count

            # Merge the counts
            d1 = Counter(qsr_counts)
            d2 = Counter(qmr_counts)
            merge = dict(d1 + d2)

            # If condition data exists (qsr or qmr provided) but no defects found, LoF = 1.0
            # According to the manual, LoF = 0 only when condition assessment data is missing,
            # not when defects are zero. When data exists with no defects, LoF must be 1.0.
            if not merge and (qsr or qmr):
                return {"overall": "0000", "lof": 1.0}

            # Sort by grade (descending)
            sorted_dict = dict(OrderedDict(sorted(merge.items(), reverse=True)))

            # Take first 2 pairs
            first2pairs = {k: sorted_dict[k] for k in list(sorted_dict)[:2]}
            f2p_list = list(first2pairs.items())

            index1, index2, index3, index4 = 0, 0, 0, 0
            index2_char = 0

            try:
                index1 = f2p_list[0][0]
            except IndexError:
                pass

            try:
                index2 = f2p_list[0][1]
                index2_char = QualityRatingCalculator._convert_to_alphabet(f2p_list[0][1])
            except IndexError:
                pass

            try:
                index3 = f2p_list[1][0]
            except IndexError:
                pass

            try:
                index4 = f2p_list[1][1]
            except IndexError:
                pass

            lof_re = 0.0

            try:
                lof_baser = f"{index1}{index2_char}"
                lof_index = int(lof_baser)
                lof_re = lof_index / 10
            except (ValueError, TypeError):
                lof_index_re = f"{index1}0"
                lof_index_re_mod = int(lof_index_re) / 10
                lof_re = lof_index_re_mod + 1

            raw_converted = f"{index1}{QualityRatingCalculator._convert_to_alphabet(index2)}{index3}{QualityRatingCalculator._convert_to_alphabet(index4)}"

            return {"overall": raw_converted, "lof": lof_re}

        except Exception:
            # Log the error in production
            # logger.error(f"Error calculating overall quick rating: {e}")
            return {"overall": "0000", "lof": 0.0}

    @staticmethod
    def _convert_to_alphabet(count: Union[int, float]) -> str:
        """Convert count to alphabet representation."""
        if count < 10:
            return str(int(count))

        if count > 135:
            return "Z"

        alpha_index = math.floor((count - 10) / 5)
        return chr(ord("A") + alpha_index)

    @staticmethod
    def _extract_count_from_quality_rating(quality_rating: str, grade: int) -> int:
        """Extract count for a specific grade from quality rating string."""
        if not isinstance(quality_rating, str):
            return 0

        for i in range(0, len(quality_rating), 2):
            if i + 1 < len(quality_rating):
                try:
                    rating_grade = int(quality_rating[i])
                    count_str = quality_rating[i + 1]

                    if rating_grade == grade:
                        if count_str.isdigit():
                            return int(count_str)
                        else:
                            return QualityRatingCalculator._convert_alphabet_to_count(count_str)
                except (ValueError, IndexError):
                    continue

        return 0

    @staticmethod
    def _convert_alphabet_to_count(alpha: str) -> int:
        """Convert alphabet representation back to count."""
        if not isinstance(alpha, str) or len(alpha) != 1:
            return 0

        if alpha.isdigit():
            return int(alpha)

        alpha_index = ord(alpha.upper()) - ord("A")
        return 10 + (alpha_index * 5)


class RatingIndexCalculator:
    """Calculator for rating indices (SPRI, MPRI, OPRI)."""

    @staticmethod
    def calculate_rating_index(total_points: float, total_count: float) -> str:
        """Calculate rating index."""
        if total_count == 0:
            return "0"

        if total_points < 0 or total_count < 0:
            raise ValueError("Total points and count must be non-negative")

        index = total_points / total_count
        return f"{index:.1f}"


class DefectClassifier(Protocol):
    """Protocol for defect classification."""

    def is_defect_type(self, observation: ObservationUnion, defect_type: DefectType) -> bool:
        """Check if an observation is of a specific defect type."""
        ...


class UniversalDefectClassifier:
    """Universal classifier for determining defect types across all inspection types."""

    def __init__(self):
        """Initialize the universal defect classifier."""
        self.nassco_defects = NasscoDefects
        self.wrc_defects = WrcDefects

    def is_defect_type(self, observation: ObservationUnion, defect_type: DefectType) -> bool:
        """Check if an observation is of a specific defect type."""
        try:
            # Handle different observation types
            if isinstance(observation, (PACPObservation, MACPObservation)):
                # Use NasscoDefects for PACP and MACP observations
                registry_defect_type = self.nassco_defects.get_defect_type(observation.code)
                if registry_defect_type:
                    return registry_defect_type == defect_type
            elif isinstance(observation, (MainlineObservation, ManholeObservation)):
                # Use WrcDefects for Mainline and Manhole observations
                registry_defect_type = self.wrc_defects.get_defect_type(observation.code)
                if registry_defect_type:
                    return registry_defect_type == defect_type
            return False
        except (KeyError, AttributeError):
            # If the code is not in the registry, return False
            return False


class ObservationProcessor:
    """Processor for handling observations from all inspection types with validation."""

    def __init__(self, is_imperial: bool = True):
        self.is_imperial = is_imperial
        self.mainline_division_factor = 5 if is_imperial else 1.5
        self.manhole_division_factor = 1 if is_imperial else 0.3

    def process_observations(self, observations: List[ObservationUnion]) -> tuple[List[ObservationUnion], Dict[str, ContinuousDefect]]:
        """Process observations to separate point and continuous defects."""
        if not isinstance(observations, list):
            raise TypeError("Observations must be a list")

        point_defects = []
        continuous_defects = {}

        for obs in observations:
            if not isinstance(obs, (PACPObservation, MACPObservation, MainlineObservation, ManholeObservation)):
                continue

            if not obs.severity or not 1 <= obs.severity <= 5:
                continue

            if obs.continuous_defect:
                self._process_continuous_defect(obs, continuous_defects)
            else:
                point_defects.append(obs)

        # Validate continuous defects
        for defect in continuous_defects.values():
            defect.validate_severity_consistency()
            if defect.start_distance is None or defect.end_distance is None:
                raise ValueError(f"Continuous defect {defect.defect_id} missing start or end")

        return point_defects, continuous_defects

    def _process_continuous_defect(self, obs: ObservationUnion, continuous_defects: Dict[str, ContinuousDefect]) -> None:
        """Process a continuous defect observation."""
        if not obs.continuous_defect or len(obs.continuous_defect) < 2:
            raise ValueError(f"Invalid continuous defect format: {obs.continuous_defect}")

        defect_id = obs.continuous_defect[1:]  # Remove S/F prefix
        defect_type = obs.continuous_defect[0].upper()  # S for Start, F for Finish

        if defect_type not in ["S", "F"]:
            raise ValueError(f"Invalid continuous defect type: {defect_type}")

        if defect_id not in continuous_defects:
            continuous_defects[defect_id] = ContinuousDefect(defect_id=defect_id, severity=obs.severity)

        defect = continuous_defects[defect_id]

        if defect_type == "S":
            if defect.start_distance is not None:
                raise ValueError(f"Continuous defect {defect_id} has multiple starts")
            defect.start_distance = obs.distance
            defect.start_severity = obs.severity
        elif defect_type == "F":
            if defect.end_distance is not None:
                raise ValueError(f"Continuous defect {defect_id} has multiple ends")
            defect.end_distance = obs.distance
            defect.end_severity = obs.severity

        # Calculate length if both start and end are available
        if defect.start_distance is not None and defect.end_distance is not None:
            if isinstance(obs, (PACPObservation, MainlineObservation)):
                division_factor = self.mainline_division_factor
            elif isinstance(obs, (MACPObservation, ManholeObservation)):
                division_factor = self.manhole_division_factor
            else:
                raise ValueError(f"Invalid observation type: {type(obs)}")
            defect.calculate_length(division_factor)


class DefectRatingsCalculator:
    """Calculator for defect-specific ratings."""

    def __init__(self, defect_classifier: DefectClassifier):
        """
        Initialize the defect ratings calculator.

        Args:
            defect_classifier: Classifier for determining defect types
        """
        self.defect_classifier = defect_classifier

    def calculate_defect_ratings(
        self, point_defects: List[ObservationUnion], continuous_defects: Dict[str, ContinuousDefect], defect_type: DefectType, all_observations: List[ObservationUnion]
    ) -> DefectRatings:
        """Calculate ratings for a specific defect type."""
        ratings = DefectRatings()

        # Count point defects
        for obs in point_defects:
            if self.defect_classifier.is_defect_type(obs, defect_type):
                ratings.add_point_defect(obs.severity)

        # Count continuous defects
        for defect in continuous_defects.values():
            if defect.length is not None:
                # Find observations that are part of this continuous defect
                matching_obs = [obs for obs in all_observations if (obs.continuous_defect and obs.continuous_defect[1:] == defect.defect_id and obs.severity == defect.severity)]

                if matching_obs and self.defect_classifier.is_defect_type(matching_obs[0], defect_type):
                    ratings.add_continuous_defect(defect.severity, defect.length)

        return ratings


class PipeRatingsCalculator:
    """
    Calculator for pipe ratings based on inspection data.

    This class calculates various pipe rating metrics including:
    - QSR (Quality Structural Rating)
    - QMR (Quality Maintenance Rating)
    - QOR (Quality Overall Rating)
    - SPR (Structural Point Rating)
    - MPR (Maintenance Point Rating)
    - OPR (Overall Point Rating)
    - SPRI (Structural Point Rating Index)
    - MPRI (Maintenance Point Rating Index)
    - OPRI (Overall Point Rating Index)
    - LOF (Likelihood of Failure)

    Supports all inspection types: PACP, MACP, Mainline, and Manhole inspections.
    """

    def __init__(self, is_imperial: bool = True, defect_classifier: Optional[DefectClassifier] = None):
        """
        Initialize the pipe ratings calculator.

        Args:
            is_imperial: Whether measurements are in imperial units (True) or metric (False)
            defect_classifier: Optional custom defect classifier
        """
        self.observation_processor = ObservationProcessor(is_imperial)
        self.defect_classifier = defect_classifier or UniversalDefectClassifier()
        self.defect_ratings_calculator = DefectRatingsCalculator(self.defect_classifier)

    def calculate_ratings(self, inspection: InspectionUnion) -> PipeRatings:
        """
        Calculate all pipe ratings from an inspection.

        Args:
            inspection: Inspection data (PACP, MACP, Mainline, or Manhole)

        Returns:
            PipeRatings object containing all calculated ratings

        Raises:
            ValueError: If inspection data is invalid
            TypeError: If inspection is not a valid inspection type
        """
        if not hasattr(inspection, "observations"):
            raise TypeError("Inspection must have an observations attribute")

        if not inspection.observations:
            return PipeRatings()

        try:
            # Process observations to separate point and continuous defects
            point_defects, continuous_defects = self.observation_processor.process_observations(inspection.observations)

            # Calculate structural and maintenance ratings
            structural_ratings = self.defect_ratings_calculator.calculate_defect_ratings(point_defects, continuous_defects, DefectType.structural, inspection.observations)
            maintenance_ratings = self.defect_ratings_calculator.calculate_defect_ratings(
                point_defects, continuous_defects, DefectType.operational_and_maintenance, inspection.observations
            )

            # Calculate quality ratings
            qsr = QualityRatingCalculator.calculate_quality_rating(structural_ratings)
            qmr = QualityRatingCalculator.calculate_quality_rating(maintenance_ratings)
            qor = QualityRatingCalculator.calculate_overall_quality_rating(qsr, qmr)

            # Calculate point ratings
            spr = structural_ratings.total_points
            mpr = maintenance_ratings.total_points
            opr = spr + mpr

            # Calculate rating indices
            spri = RatingIndexCalculator.calculate_rating_index(spr, structural_ratings.total_count)
            mpri = RatingIndexCalculator.calculate_rating_index(mpr, maintenance_ratings.total_count)
            opri = RatingIndexCalculator.calculate_rating_index(opr, structural_ratings.total_count + maintenance_ratings.total_count)

            # Calculate likelihood of failure (LOF)
            quick_rating_result = QualityRatingCalculator.overall_quick_rating(qsr, qmr)
            lof = quick_rating_result["lof"]

            # Calculate critical count (severity 5)
            critical_count = structural_ratings.severity_counts.severity_5 + maintenance_ratings.severity_counts.severity_5

            return PipeRatings(
                qsr=qsr,
                qmr=qmr,
                qor=qor,
                spr=spr,
                mpr=mpr,
                opr=opr,
                spri=spri,
                mpri=mpri,
                opri=opri,
                lof=lof,
                structural_count=structural_ratings.total_count,
                maintenance_count=maintenance_ratings.total_count,
                critical_count=critical_count,
                structural_severity_1=structural_ratings.severity_counts.severity_1,
                structural_severity_2=structural_ratings.severity_counts.severity_2,
                structural_severity_3=structural_ratings.severity_counts.severity_3,
                structural_severity_4=structural_ratings.severity_counts.severity_4,
                structural_severity_5=structural_ratings.severity_counts.severity_5,
                maintenance_severity_1=maintenance_ratings.severity_counts.severity_1,
                maintenance_severity_2=maintenance_ratings.severity_counts.severity_2,
                maintenance_severity_3=maintenance_ratings.severity_counts.severity_3,
                maintenance_severity_4=maintenance_ratings.severity_counts.severity_4,
                maintenance_severity_5=maintenance_ratings.severity_counts.severity_5,
            )

        except Exception as e:
            # Log the error in production
            # logger.error(f"Error calculating pipe ratings: {e}")
            raise ValueError(f"Failed to calculate pipe ratings: {e}") from e


def calculate_pipe_ratings(inspection: InspectionUnion, is_imperial: bool = True) -> PipeRatings:
    """
    Convenience function to calculate pipe ratings from an inspection.

    Args:
        inspection: Inspection data (PACP, MACP, Mainline, or Manhole)
        is_imperial: Whether measurements are in imperial units

    Returns:
        PipeRatings object containing all calculated ratings

    Raises:
        ValueError: If inspection data is invalid
        TypeError: If inspection is not a valid inspection type
    """
    calculator = PipeRatingsCalculator(is_imperial)
    return calculator.calculate_ratings(inspection)
