"""Term-structure interface for curve primitives."""

from __future__ import annotations

from abc import ABC, abstractmethod

from fuggers_py.core.daycounts import DayCountConvention
from fuggers_py.core.types import Date

from .errors import InvalidCurveInput, TenorOutOfBounds
from .value_type import ValueType


class TermStructure(ABC):
    @abstractmethod
    def reference_date(self) -> Date:
        """Anchor date for tenors."""

    @abstractmethod
    def value_at(self, t: float) -> float:
        """Raw curve value at tenor `t` (years)."""

    @abstractmethod
    def tenor_bounds(self) -> tuple[float, float]:
        """Return inclusive tenor bounds (min, max)."""

    @abstractmethod
    def value_type(self) -> ValueType:
        """Describe the semantic meaning of `value_at`."""

    def derivative_at(self, t: float) -> float | None:  # noqa: D401 - part of trait
        """Derivative of `value_at` w.r.t. tenor, if supported."""

        return None

    @abstractmethod
    def max_date(self) -> Date:
        """Return the maximum supported curve date."""

    def in_range(self, t: float) -> bool:
        lo, hi = self.tenor_bounds()
        tau = float(t)
        return lo <= tau <= hi

    def has_derivative(self) -> bool:
        return self.derivative_at(1.0) is not None

    def try_value_at(self, t: float) -> float:
        tau = float(t)
        if tau < 0.0:
            raise InvalidCurveInput("Tenor must be non-negative.")
        lo, hi = self.tenor_bounds()
        if tau < lo or tau > hi:
            raise TenorOutOfBounds(t=tau, min=lo, max=hi)
        return float(self.value_at(tau))

    def value_at_date(self, date: Date) -> float:
        return self.value_at(self.date_to_tenor(date))

    def date_to_tenor(self, date: Date, *, day_count: DayCountConvention = DayCountConvention.ACT_365_FIXED) -> float:
        yf = day_count.to_day_count().year_fraction(self.reference_date(), date)
        return float(yf)

    def tenor_to_date(self, t: float) -> Date:
        days = int(round(float(t) * 365.0))
        return self.reference_date().add_days(days)
