"""Exception hierarchy for `fuggers_py.math`.

Numerical and validation failures raise structured exceptions with relevant
payload.
"""

from __future__ import annotations

from dataclasses import dataclass


class MathError(Exception):
    """Base exception for all math-layer failures."""

    @classmethod
    def convergence_failed(cls, iterations: int, residual: float) -> ConvergenceFailed:
        return ConvergenceFailed(iterations=iterations, residual=residual)

    @classmethod
    def invalid_input(cls, reason: str) -> InvalidInput:
        return InvalidInput(reason=reason)

    @classmethod
    def insufficient_data(cls, required: int, actual: int) -> InsufficientData:
        return InsufficientData(required=required, actual=actual)


@dataclass(frozen=True, slots=True)
class ConvergenceFailed(MathError):
    iterations: int
    residual: float

    def __str__(self) -> str:
        return (
            f"Convergence failed after {self.iterations} iterations "
            f"(residual={self.residual:.6g})."
        )


@dataclass(frozen=True, slots=True)
class InvalidBracket(MathError):
    a: float
    b: float
    fa: float
    fb: float

    def __str__(self) -> str:
        return (
            "Invalid bracket: f(a) and f(b) must have opposite signs "
            f"(a={self.a:.6g}, b={self.b:.6g}, fa={self.fa:.6g}, fb={self.fb:.6g})."
        )


@dataclass(frozen=True, slots=True)
class DivisionByZero(MathError):
    value: float

    def __str__(self) -> str:
        return f"Division by zero (value={self.value:.6g})."


@dataclass(frozen=True, slots=True)
class SingularMatrix(MathError):
    def __str__(self) -> str:  # pragma: no cover - trivial
        return "Singular matrix."


@dataclass(frozen=True, slots=True)
class DimensionMismatch(MathError):
    rows1: int
    cols1: int
    rows2: int
    cols2: int

    def __str__(self) -> str:
        return (
            "Dimension mismatch: "
            f"({self.rows1}x{self.cols1}) vs ({self.rows2}x{self.cols2})."
        )


@dataclass(frozen=True, slots=True)
class ExtrapolationNotAllowed(MathError):
    x: float
    min: float
    max: float

    def __str__(self) -> str:
        return (
            f"Extrapolation not allowed at x={self.x:.6g} "
            f"(range=[{self.min:.6g}, {self.max:.6g}])."
        )


@dataclass(frozen=True, slots=True)
class InsufficientData(MathError):
    required: int
    actual: int

    def __str__(self) -> str:
        return f"Insufficient data: required {self.required}, got {self.actual}."


@dataclass(frozen=True, slots=True)
class InvalidInput(MathError):
    reason: str

    def __str__(self) -> str:  # pragma: no cover - trivial
        return f"Invalid input: {self.reason}"


@dataclass(frozen=True, slots=True)
class MathOverflow(MathError):
    operation: str

    def __str__(self) -> str:  # pragma: no cover - trivial
        return f"Math overflow during {self.operation}."


@dataclass(frozen=True, slots=True)
class MathUnderflow(MathError):
    operation: str

    def __str__(self) -> str:  # pragma: no cover - trivial
        return f"Math underflow during {self.operation}."
