"""Value conversion utilities for curve primitives."""

from __future__ import annotations

import math

from fuggers_py.core.types import Compounding

from .errors import InvalidCurveInput


def _require_positive(value: float, *, name: str) -> float:
    numeric = float(value)
    if numeric <= 0.0 or not math.isfinite(numeric):
        raise InvalidCurveInput(f"{name} must be positive and finite.")
    return numeric


def _require_probability(value: float, *, name: str) -> float:
    numeric = float(value)
    if not math.isfinite(numeric) or numeric < 0.0 or numeric > 1.0:
        raise InvalidCurveInput(f"{name} must lie in [0, 1].")
    return numeric


class ValueConverter:
    @staticmethod
    def df_to_zero(discount_factor: float, t: float, compounding: Compounding) -> float:
        df = float(discount_factor)
        tau = float(t)
        if tau == 0.0:
            return 0.0
        if tau < 0.0:
            raise InvalidCurveInput("Tenor must be non-negative.")
        df = _require_positive(df, name="discount_factor")

        match compounding:
            case Compounding.CONTINUOUS:
                return float(-math.log(df) / tau)
            case Compounding.SIMPLE:
                return float((1.0 / df - 1.0) / tau)
            case Compounding.ANNUAL:
                return float(df ** (-1.0 / tau) - 1.0)
            case Compounding.SEMI_ANNUAL:
                return float(2.0 * (df ** (-1.0 / (2.0 * tau)) - 1.0))
            case Compounding.QUARTERLY:
                return float(4.0 * (df ** (-1.0 / (4.0 * tau)) - 1.0))
            case Compounding.MONTHLY:
                return float(12.0 * (df ** (-1.0 / (12.0 * tau)) - 1.0))
            case Compounding.DAILY:
                return float(365.0 * (df ** (-1.0 / (365.0 * tau)) - 1.0))
            case _:
                raise InvalidCurveInput(f"Unsupported compounding convention: {compounding!r}.")

    @staticmethod
    def zero_to_df(zero_rate: float, t: float, compounding: Compounding) -> float:
        r = float(zero_rate)
        tau = float(t)
        if tau == 0.0:
            return 1.0
        if tau < 0.0:
            raise InvalidCurveInput("Tenor must be non-negative.")

        match compounding:
            case Compounding.CONTINUOUS:
                return float(math.exp(-r * tau))
            case Compounding.SIMPLE:
                if 1.0 + r * tau <= 0.0:
                    raise InvalidCurveInput("Simple-compounded zero rate implies a non-positive discount factor.")
                return float(1.0 / (1.0 + r * tau))
            case Compounding.ANNUAL:
                if 1.0 + r <= 0.0:
                    raise InvalidCurveInput("Annual-compounded zero rate implies a non-positive discount factor.")
                return float((1.0 + r) ** (-tau))
            case Compounding.SEMI_ANNUAL:
                if 1.0 + r / 2.0 <= 0.0:
                    raise InvalidCurveInput("Semi-annual zero rate implies a non-positive discount factor.")
                return float((1.0 + r / 2.0) ** (-2.0 * tau))
            case Compounding.QUARTERLY:
                if 1.0 + r / 4.0 <= 0.0:
                    raise InvalidCurveInput("Quarterly zero rate implies a non-positive discount factor.")
                return float((1.0 + r / 4.0) ** (-4.0 * tau))
            case Compounding.MONTHLY:
                if 1.0 + r / 12.0 <= 0.0:
                    raise InvalidCurveInput("Monthly zero rate implies a non-positive discount factor.")
                return float((1.0 + r / 12.0) ** (-12.0 * tau))
            case Compounding.DAILY:
                if 1.0 + r / 365.0 <= 0.0:
                    raise InvalidCurveInput("Daily zero rate implies a non-positive discount factor.")
                return float((1.0 + r / 365.0) ** (-365.0 * tau))
            case _:
                raise InvalidCurveInput(f"Unsupported compounding convention: {compounding!r}.")

    @staticmethod
    def _to_continuous(rate: float, compounding: Compounding) -> float:
        r = float(rate)
        match compounding:
            case Compounding.CONTINUOUS:
                return r
            case Compounding.SIMPLE | Compounding.ANNUAL:
                if 1.0 + r <= 0.0:
                    raise InvalidCurveInput("Rate implies a non-positive one-year accumulation factor.")
                return float(math.log(1.0 + r))
            case Compounding.SEMI_ANNUAL:
                if 1.0 + r / 2.0 <= 0.0:
                    raise InvalidCurveInput("Rate implies a non-positive semi-annual accumulation factor.")
                return float(2.0 * math.log(1.0 + r / 2.0))
            case Compounding.QUARTERLY:
                if 1.0 + r / 4.0 <= 0.0:
                    raise InvalidCurveInput("Rate implies a non-positive quarterly accumulation factor.")
                return float(4.0 * math.log(1.0 + r / 4.0))
            case Compounding.MONTHLY:
                if 1.0 + r / 12.0 <= 0.0:
                    raise InvalidCurveInput("Rate implies a non-positive monthly accumulation factor.")
                return float(12.0 * math.log(1.0 + r / 12.0))
            case Compounding.DAILY:
                if 1.0 + r / 365.0 <= 0.0:
                    raise InvalidCurveInput("Rate implies a non-positive daily accumulation factor.")
                return float(365.0 * math.log(1.0 + r / 365.0))
            case _:
                raise InvalidCurveInput(f"Unsupported compounding convention: {compounding!r}.")

    @staticmethod
    def _from_continuous(continuous_rate: float, target_compounding: Compounding) -> float:
        rc = float(continuous_rate)
        match target_compounding:
            case Compounding.CONTINUOUS:
                return rc
            case Compounding.SIMPLE | Compounding.ANNUAL:
                return float(math.exp(rc) - 1.0)
            case Compounding.SEMI_ANNUAL:
                return float(2.0 * (math.exp(rc / 2.0) - 1.0))
            case Compounding.QUARTERLY:
                return float(4.0 * (math.exp(rc / 4.0) - 1.0))
            case Compounding.MONTHLY:
                return float(12.0 * (math.exp(rc / 12.0) - 1.0))
            case Compounding.DAILY:
                return float(365.0 * (math.exp(rc / 365.0) - 1.0))
            case _:
                raise InvalidCurveInput(f"Unsupported compounding convention: {target_compounding!r}.")

    @staticmethod
    def convert_compounding(rate: float, from_compounding: Compounding, to_compounding: Compounding) -> float:
        if from_compounding == to_compounding:
            return float(rate)
        return ValueConverter._from_continuous(
            ValueConverter._to_continuous(rate, from_compounding),
            to_compounding,
        )

    @staticmethod
    def instantaneous_forward(zero_rate: float, d_zero_dt: float, t: float) -> float:
        return float(zero_rate) + float(t) * float(d_zero_dt)

    @staticmethod
    def forward_rate_from_zeros(zero1: float, zero2: float, t1: float, t2: float) -> float:
        dt = float(t2) - float(t1)
        if abs(dt) < 1e-10:
            return float(zero2)
        return float((float(t2) * float(zero2) - float(t1) * float(zero1)) / dt)

    @staticmethod
    def forward_rate_from_dfs(
        df1: float,
        df2: float,
        t1: float,
        t2: float,
        compounding: Compounding,
    ) -> float:
        dt = float(t2) - float(t1)
        if dt <= 0.0:
            raise InvalidCurveInput("Forward-rate end tenor must be greater than start tenor.")
        df_start = _require_positive(df1, name="df1")
        df_end = _require_positive(df2, name="df2")

        ratio = df_start / df_end
        match compounding:
            case Compounding.CONTINUOUS:
                return float(math.log(ratio) / dt)
            case Compounding.SIMPLE:
                return float((ratio - 1.0) / dt)
            case _:
                forward_cont = float(math.log(ratio) / dt)
                return ValueConverter._from_continuous(forward_cont, compounding)

    @staticmethod
    def survival_to_hazard(survival_prob: float, d_survival_dt: float) -> float:
        sp = _require_probability(survival_prob, name="survival_prob")
        if sp == 0.0:
            raise InvalidCurveInput("survival_prob must be strictly positive for hazard conversion.")
        hazard = float(-float(d_survival_dt) / sp)
        if hazard < -1e-12:
            raise InvalidCurveInput("Survival probability slope implies a negative hazard rate.")
        return max(hazard, 0.0)

    @staticmethod
    def hazard_to_survival(hazard_rate: float, t: float) -> float:
        if float(t) < 0.0:
            raise InvalidCurveInput("Tenor must be non-negative.")
        if float(hazard_rate) < 0.0:
            raise InvalidCurveInput("hazard_rate must be non-negative.")
        return float(math.exp(-float(hazard_rate) * float(t)))

    @staticmethod
    def implied_hazard_rate(survival_prob: float, t: float) -> float:
        tau = float(t)
        if tau == 0.0:
            return 0.0
        if tau < 0.0:
            raise InvalidCurveInput("Tenor must be non-negative.")
        sp = _require_probability(survival_prob, name="survival_prob")
        if sp == 0.0:
            raise InvalidCurveInput("survival_prob must be strictly positive for hazard conversion.")
        return float(-math.log(sp) / tau)

    @staticmethod
    def risky_discount_factor(df: float, survival_prob: float, recovery_rate: float) -> float:
        discount_factor = _require_positive(df, name="df")
        sp = _require_probability(survival_prob, name="survival_prob")
        rec = _require_probability(recovery_rate, name="recovery_rate")
        return discount_factor * float(sp + (1.0 - sp) * rec)
