"""Reference-data records for research workflows."""

from __future__ import annotations

from dataclasses import dataclass, field, replace
from decimal import Decimal
from enum import Enum
from typing import Protocol, runtime_checkable

from fuggers_py.bonds.instruments import CallableBondBuilder, FixedBondBuilder, FloatingRateNoteBuilder, ZeroCouponBond
from fuggers_py.bonds.types import RateIndex, YieldCalculationRules
from fuggers_py.core.types import Currency, Date, Frequency

from .ids import EtfId, InstrumentId
from .market_data import EtfHolding


def _to_decimal(value: object) -> Decimal:
    if isinstance(value, Decimal):
        return value
    return Decimal(str(value))


class BondType(str, Enum):
    FIXED_RATE = "FIXED_RATE"
    ZERO_COUPON = "ZERO_COUPON"
    CALLABLE = "CALLABLE"
    PUTTABLE = "PUTTABLE"
    CALLABLE_AND_PUTTABLE = "CALLABLE_AND_PUTTABLE"
    FLOATING_RATE = "FLOATING_RATE"


class IssuerType(str, Enum):
    SOVEREIGN = "SOVEREIGN"
    AGENCY = "AGENCY"
    CORPORATE = "CORPORATE"
    MUNICIPAL = "MUNICIPAL"
    SUPRANATIONAL = "SUPRANATIONAL"
    ETF = "ETF"
    OTHER = "OTHER"


@dataclass(frozen=True, slots=True)
class CallScheduleEntry:
    exercise_date: Date
    price: Decimal

    def __post_init__(self) -> None:
        object.__setattr__(self, "price", _to_decimal(self.price))


@dataclass(frozen=True, slots=True)
class FloatingRateTerms:
    index_name: str
    spread: Decimal
    reset_frequency: Frequency
    current_reference_rate: Decimal | None = None
    cap_rate: Decimal | None = None
    floor_rate: Decimal | None = None

    def __post_init__(self) -> None:
        object.__setattr__(self, "index_name", self.index_name.strip().upper())
        object.__setattr__(self, "spread", _to_decimal(self.spread))
        if self.current_reference_rate is not None:
            object.__setattr__(self, "current_reference_rate", _to_decimal(self.current_reference_rate))
        if self.cap_rate is not None:
            object.__setattr__(self, "cap_rate", _to_decimal(self.cap_rate))
        if self.floor_rate is not None:
            object.__setattr__(self, "floor_rate", _to_decimal(self.floor_rate))

    def rate_index(self) -> RateIndex:
        return RateIndex[self.index_name]


@dataclass(frozen=True, slots=True)
class BondReferenceData:
    instrument_id: InstrumentId
    bond_type: BondType
    issuer_type: IssuerType
    issue_date: Date
    maturity_date: Date
    currency: Currency = Currency.USD
    notional: Decimal = Decimal(100)
    coupon_rate: Decimal | None = None
    frequency: Frequency | None = None
    yield_rules: YieldCalculationRules | None = None
    floating_rate_terms: FloatingRateTerms | None = None
    call_schedule: tuple[CallScheduleEntry, ...] = field(default_factory=tuple)
    put_schedule: tuple[CallScheduleEntry, ...] = field(default_factory=tuple)
    issuer_name: str | None = None
    sector: str | None = None
    rating: str | None = None

    def __post_init__(self) -> None:
        object.__setattr__(self, "instrument_id", InstrumentId.parse(self.instrument_id))
        object.__setattr__(self, "notional", _to_decimal(self.notional))
        if self.coupon_rate is not None:
            object.__setattr__(self, "coupon_rate", _to_decimal(self.coupon_rate))
        ordered_calls = tuple(sorted(self.call_schedule, key=lambda item: item.exercise_date))
        ordered_puts = tuple(sorted(self.put_schedule, key=lambda item: item.exercise_date))
        object.__setattr__(self, "call_schedule", ordered_calls)
        object.__setattr__(self, "put_schedule", ordered_puts)
        if self.issuer_name is not None:
            object.__setattr__(self, "issuer_name", self.issuer_name.strip())
        if self.sector is not None:
            object.__setattr__(self, "sector", self.sector.strip())
        if self.rating is not None:
            object.__setattr__(self, "rating", self.rating.strip())

    def to_instrument(self):
        if self.bond_type is BondType.ZERO_COUPON:
            return ZeroCouponBond(
                _issue_date=self.issue_date,
                _maturity_date=self.maturity_date,
                _currency=self.currency,
                _notional=self.notional,
                _rules=self._yield_rules(self.frequency or Frequency.ANNUAL),
            )

        if self.bond_type is BondType.FLOATING_RATE:
            if self.floating_rate_terms is None:
                raise ValueError("Floating-rate reference data requires floating_rate_terms.")
            terms = self.floating_rate_terms
            return (
                FloatingRateNoteBuilder.new()
                .with_issue_date(self.issue_date)
                .with_maturity_date(self.maturity_date)
                .with_index(terms.rate_index())
                .with_quoted_spread(terms.spread)
                .with_frequency(terms.reset_frequency)
                .with_currency(self.currency)
                .with_notional(self.notional)
                .with_rules(self._yield_rules(terms.reset_frequency))
                .with_current_reference_rate(terms.current_reference_rate or Decimal(0))
                .build()
            )

        if self.coupon_rate is None:
            raise ValueError("Fixed-rate style reference data requires coupon_rate.")
        frequency = self.frequency or Frequency.SEMI_ANNUAL
        base_bond = (
            FixedBondBuilder.new()
            .with_issue_date(self.issue_date)
            .with_maturity_date(self.maturity_date)
            .with_coupon_rate(self.coupon_rate)
            .with_frequency(frequency)
            .with_currency(self.currency)
            .with_notional(self.notional)
            .with_rules(self._yield_rules(frequency))
            .build()
        )
        if self.bond_type is BondType.FIXED_RATE:
            return base_bond

        builder = CallableBondBuilder.new().with_base_bond(base_bond)
        for entry in self.call_schedule:
            builder = builder.add_call(call_date=entry.exercise_date, call_price=entry.price)
        for entry in self.put_schedule:
            builder = builder.add_put(put_date=entry.exercise_date, put_price=entry.price)
        return builder.build()

    def _yield_rules(self, frequency: Frequency) -> YieldCalculationRules:
        if self.yield_rules is not None:
            if self.yield_rules.frequency is frequency:
                return self.yield_rules
            return replace(self.yield_rules, frequency=frequency)
        if self.issuer_type is IssuerType.CORPORATE:
            rules = YieldCalculationRules.us_corporate()
        else:
            rules = YieldCalculationRules.us_treasury()
        if rules.frequency is frequency:
            return rules
        return replace(rules, frequency=frequency)


@dataclass(frozen=True, slots=True)
class IssuerReferenceData:
    issuer_name: str
    issuer_type: IssuerType = IssuerType.OTHER
    issuer_id: str | None = None
    country: str | None = None
    sector: str | None = None
    rating: str | None = None

    def __post_init__(self) -> None:
        object.__setattr__(self, "issuer_name", self.issuer_name.strip())
        if not self.issuer_name:
            raise ValueError("issuer_name must be non-empty.")
        if self.issuer_id is not None:
            object.__setattr__(self, "issuer_id", self.issuer_id.strip())
        if self.country is not None:
            object.__setattr__(self, "country", self.country.strip().upper())
        if self.sector is not None:
            object.__setattr__(self, "sector", self.sector.strip())
        if self.rating is not None:
            object.__setattr__(self, "rating", self.rating.strip())


@dataclass(frozen=True, slots=True)
class RatingRecord:
    rating: str
    agency: str | None = None
    outlook: str | None = None
    instrument_id: InstrumentId | None = None
    issuer_name: str | None = None
    effective_date: Date | None = None

    def __post_init__(self) -> None:
        object.__setattr__(self, "rating", self.rating.strip())
        if not self.rating:
            raise ValueError("rating must be non-empty.")
        if self.agency is not None:
            object.__setattr__(self, "agency", self.agency.strip())
        if self.outlook is not None:
            object.__setattr__(self, "outlook", self.outlook.strip())
        if self.instrument_id is not None:
            object.__setattr__(self, "instrument_id", InstrumentId.parse(self.instrument_id))
        if self.issuer_name is not None:
            object.__setattr__(self, "issuer_name", self.issuer_name.strip())


@runtime_checkable
class BondReferenceSource(Protocol):
    def get_bond_reference(self, instrument_id: InstrumentId | str) -> BondReferenceData | None:
        ...


@runtime_checkable
class IssuerReferenceSource(Protocol):
    def get_issuer_reference(self, issuer_name: str) -> IssuerReferenceData | None:
        ...


@runtime_checkable
class RatingSource(Protocol):
    def get_rating(self, *, instrument_id: InstrumentId | str | None = None, issuer_name: str | None = None) -> RatingRecord | None:
        ...


@runtime_checkable
class EtfHoldingsSource(Protocol):
    def get_etf_holdings(self, etf_id: EtfId | str) -> tuple[EtfHolding, ...]:
        ...


@dataclass(slots=True)
class ReferenceDataProvider:
    bond_source: BondReferenceSource | None = None
    issuer_source: IssuerReferenceSource | None = None
    rating_source: RatingSource | None = None
    etf_holdings_source: EtfHoldingsSource | None = None

    def get_bond_reference(self, instrument_id: InstrumentId | str) -> BondReferenceData | None:
        if self.bond_source is None:
            return None
        return self.bond_source.get_bond_reference(instrument_id)

    def get_issuer_reference(self, issuer_name: str) -> IssuerReferenceData | None:
        if self.issuer_source is None:
            return None
        return self.issuer_source.get_issuer_reference(issuer_name)

    def get_rating(self, *, instrument_id: InstrumentId | str | None = None, issuer_name: str | None = None) -> RatingRecord | None:
        if self.rating_source is None:
            return None
        return self.rating_source.get_rating(instrument_id=instrument_id, issuer_name=issuer_name)

    def get_etf_holdings(self, etf_id: EtfId | str) -> tuple[EtfHolding, ...]:
        if self.etf_holdings_source is None:
            return ()
        return self.etf_holdings_source.get_etf_holdings(etf_id)


__all__ = [
    "BondReferenceData",
    "BondReferenceSource",
    "BondType",
    "CallScheduleEntry",
    "EtfHoldingsSource",
    "FloatingRateTerms",
    "IssuerReferenceData",
    "IssuerReferenceSource",
    "IssuerType",
    "RatingRecord",
    "RatingSource",
    "ReferenceDataProvider",
]
