"""Bond pricer (`fuggers_py.bonds.pricing.pricer`)."""

from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal

from fuggers_py.core.types import Compounding, Currency, Date, Price, Yield

from ..errors import BondPricingError
from ..traits import Bond
from ..types import CompoundingKind, CompoundingMethod, YieldCalculationRules
from .yield_engine import StandardYieldEngine, YieldEngineResult


def _core_compounding_for(method: CompoundingMethod) -> Compounding:
    if method.kind is CompoundingKind.CONTINUOUS:
        return Compounding.CONTINUOUS
    if method.kind in {CompoundingKind.SIMPLE, CompoundingKind.DISCOUNT}:
        return Compounding.SIMPLE
    if method.frequency == 1:
        return Compounding.ANNUAL
    if method.frequency == 2:
        return Compounding.SEMI_ANNUAL
    if method.frequency == 4:
        return Compounding.QUARTERLY
    if method.frequency == 12:
        return Compounding.MONTHLY
    return Compounding.ANNUAL


@dataclass(frozen=True, slots=True)
class PriceResult:
    """Bond price decomposition at a settlement date.

    Dirty and clean prices are represented as :class:`~fuggers_py.core.types.Price`
    values. ``present_value`` mirrors the dirty price in percent-of-par form.
    """

    dirty: Price
    clean: Price
    accrued: Decimal

    @property
    def dirty_price(self) -> Price:
        return self.dirty

    @property
    def clean_price(self) -> Price:
        return self.clean

    @property
    def accrued_interest(self) -> Decimal:
        return self.accrued

    @property
    def present_value(self) -> Decimal:
        """Return the dirty price as a percent-of-par decimal."""
        return self.dirty.as_percentage()


@dataclass(frozen=True, slots=True)
class YieldResult:
    """Bond yield result together with the lower-level engine output."""

    ytm: Yield
    engine: YieldEngineResult


@dataclass(frozen=True, slots=True)
class BondPricer:
    """Convert between bond yields and clean/dirty prices."""

    engine: StandardYieldEngine = StandardYieldEngine()

    def price_from_yield(self, bond: Bond, ytm: Yield, settlement_date: Date) -> PriceResult:
        """Return dirty and clean prices from a yield-to-maturity input.

        The yield input is converted to the bond's compounding convention before
        discounting. The returned clean price excludes accrued interest.
        """
        rules = bond.rules()
        y = self._yield_to_engine_rate(ytm, rules=rules)

        dirty = self.engine.dirty_price_from_yield(
            bond.cash_flows(),
            yield_rate=y,
            settlement_date=settlement_date,
            rules=rules,
        )
        accrued = bond.accrued_interest(settlement_date)
        clean = dirty - float(accrued)

        ccy = bond.currency()
        return PriceResult(
            dirty=Price.new(Decimal(str(dirty)), ccy),
            clean=Price.new(Decimal(str(clean)), ccy),
            accrued=accrued,
        )

    def yield_from_price(self, bond: Bond, clean_price: Price, settlement_date: Date) -> YieldResult:
        """Return the yield implied by a clean price and settlement date."""
        if clean_price.currency() != bond.currency():
            raise BondPricingError(reason="Price currency does not match bond currency.")

        rules = bond.rules()
        accrued = bond.accrued_interest(settlement_date)

        engine_res = self.engine.yield_from_price(
            bond.cash_flows(),
            clean_price=clean_price.as_percentage(),
            accrued=accrued,
            settlement_date=settlement_date,
            rules=rules,
        )

        compounding = _core_compounding_for(rules.compounding)
        ytm = Yield.new(Decimal(str(engine_res.yield_rate)), compounding=compounding)
        return YieldResult(ytm=ytm, engine=engine_res)

    @staticmethod
    def _yield_to_engine_rate(ytm: Yield, *, rules: YieldCalculationRules) -> float:
        """Convert a market yield into the engine's compounding convention."""
        target_compounding = _core_compounding_for(rules.compounding)
        y = ytm.convert_to(target_compounding).value()
        return float(y)


BondResult = PriceResult


__all__ = ["BondPricer", "PriceResult", "BondResult", "YieldResult"]
