"""Typed engine outputs and publisher contracts.

These outputs keep field units explicit: prices, yields, spreads, rates, and
risk measures are stored as raw decimals in their own fields rather than being
coerced into a single generic quote number.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from typing import Protocol, runtime_checkable

from fuggers_py.core.types import Date

from .ids import CurveId, EtfId, InstrumentId, PortfolioId
from .market_data import CurveData, CurveInputs


def _to_decimal(value: object | None) -> Decimal | None:
    """Coerce a nullable output scalar to ``Decimal``."""
    if value is None or isinstance(value, Decimal):
        return value
    return Decimal(str(value))


def _coerce_decimal_fields(instance: object, *names: str) -> None:
    """Normalize named attributes on a dataclass-like instance to decimals."""
    for name in names:
        value = getattr(instance, name)
        coerced = _to_decimal(value)
        if coerced is not None:
            object.__setattr__(instance, name, coerced)


@dataclass(frozen=True, slots=True)
class BondQuoteOutput:
    """Bond pricing output with explicit unit-bearing fields.

    ``clean_price`` and ``dirty_price`` are price levels, ``yield_*`` fields are
    yields, spread fields are raw decimal spreads, and ``pv01``/``dv01`` are
    kept distinct but may be aliased when only one is supplied.
    """

    instrument_id: InstrumentId | None = None
    pricing_path: str | None = None
    clean_price: Decimal | None = None
    dirty_price: Decimal | None = None
    accrued_interest: Decimal | None = None
    yield_to_maturity: Decimal | None = None
    yield_to_worst: Decimal | None = None
    current_yield: Decimal | None = None
    modified_duration: Decimal | None = None
    effective_duration: Decimal | None = None
    macaulay_duration: Decimal | None = None
    dv01: Decimal | None = None
    convexity: Decimal | None = None
    effective_convexity: Decimal | None = None
    key_rate_durations: dict[str, Decimal] = field(default_factory=dict)
    z_spread: Decimal | None = None
    g_spread: Decimal | None = None
    i_spread: Decimal | None = None
    oas: Decimal | None = None
    discount_margin: Decimal | None = None
    spread_duration: Decimal | None = None
    asset_swap_spread: Decimal | None = None
    benchmark_info: str | None = None
    option_value: Decimal | None = None
    projected_next_coupon: Decimal | None = None
    next_reset_date: Date | None = None
    warnings: tuple[str, ...] = ()
    notes: tuple[str, ...] = ()
    settlement_date: Date | None = None
    timestamp: datetime | None = None
    source: str | None = None
    is_stale: bool = False
    quality_score: Decimal | None = None
    bid_price: Decimal | None = None
    mid_price: Decimal | None = None
    ask_price: Decimal | None = None
    ytc: Decimal | None = None
    pv01: Decimal | None = None
    cs01: Decimal | None = None

    def __post_init__(self) -> None:
        if self.instrument_id is not None:
            object.__setattr__(self, "instrument_id", InstrumentId.parse(self.instrument_id))
        _coerce_decimal_fields(
            self,
            "clean_price",
            "dirty_price",
            "accrued_interest",
            "yield_to_maturity",
            "yield_to_worst",
            "current_yield",
            "modified_duration",
            "effective_duration",
            "macaulay_duration",
            "dv01",
            "convexity",
            "effective_convexity",
            "z_spread",
            "g_spread",
            "i_spread",
            "oas",
            "discount_margin",
            "spread_duration",
            "asset_swap_spread",
            "option_value",
            "projected_next_coupon",
            "quality_score",
            "bid_price",
            "mid_price",
            "ask_price",
            "ytc",
            "pv01",
            "cs01",
        )
        object.__setattr__(
            self,
            "key_rate_durations",
            {key: _to_decimal(value) for key, value in self.key_rate_durations.items()},
        )
        if self.source is not None:
            object.__setattr__(self, "source", self.source.strip())
        if self.pricing_path is not None:
            object.__setattr__(self, "pricing_path", self.pricing_path.strip())
        if self.benchmark_info is not None:
            object.__setattr__(self, "benchmark_info", self.benchmark_info.strip())
        object.__setattr__(self, "warnings", tuple(self.warnings))
        object.__setattr__(self, "notes", tuple(self.notes))
        if self.pv01 is None and self.dv01 is not None:
            object.__setattr__(self, "pv01", self.dv01)
        if self.mid_price is None and self.clean_price is not None:
            object.__setattr__(self, "mid_price", self.clean_price)


@dataclass(frozen=True, slots=True)
class SwapQuoteOutput:
    """Swap pricing output with raw decimal rate and PV fields."""

    instrument_id: InstrumentId | None = None
    pricing_path: str | None = None
    par_rate: Decimal | None = None
    present_value: Decimal | None = None
    fixed_leg_pv: Decimal | None = None
    floating_leg_pv: Decimal | None = None
    annuity: Decimal | None = None
    dv01: Decimal | None = None
    pv01: Decimal | None = None
    warnings: tuple[str, ...] = ()
    notes: tuple[str, ...] = ()
    settlement_date: Date | None = None
    timestamp: datetime | None = None
    source: str | None = None
    is_stale: bool = False
    quality_score: Decimal | None = None

    def __post_init__(self) -> None:
        if self.instrument_id is not None:
            object.__setattr__(self, "instrument_id", InstrumentId.parse(self.instrument_id))
        _coerce_decimal_fields(
            self,
            "par_rate",
            "present_value",
            "fixed_leg_pv",
            "floating_leg_pv",
            "annuity",
            "dv01",
            "pv01",
            "quality_score",
        )
        if self.pricing_path is not None:
            object.__setattr__(self, "pricing_path", self.pricing_path.strip())
        if self.source is not None:
            object.__setattr__(self, "source", self.source.strip())
        object.__setattr__(self, "warnings", tuple(self.warnings))
        object.__setattr__(self, "notes", tuple(self.notes))
        if self.pv01 is None and self.dv01 is not None:
            object.__setattr__(self, "pv01", self.dv01)


@dataclass(frozen=True, slots=True)
class BasisSwapQuoteOutput:
    """Basis-swap pricing output with raw decimal spread and PV fields."""

    instrument_id: InstrumentId | None = None
    pricing_path: str | None = None
    basis_spread: Decimal | None = None
    present_value: Decimal | None = None
    pay_leg_pv: Decimal | None = None
    receive_leg_pv: Decimal | None = None
    dv01: Decimal | None = None
    pv01: Decimal | None = None
    warnings: tuple[str, ...] = ()
    notes: tuple[str, ...] = ()
    settlement_date: Date | None = None
    timestamp: datetime | None = None
    source: str | None = None
    is_stale: bool = False
    quality_score: Decimal | None = None

    def __post_init__(self) -> None:
        if self.instrument_id is not None:
            object.__setattr__(self, "instrument_id", InstrumentId.parse(self.instrument_id))
        _coerce_decimal_fields(
            self,
            "basis_spread",
            "present_value",
            "pay_leg_pv",
            "receive_leg_pv",
            "dv01",
            "pv01",
            "quality_score",
        )
        if self.pricing_path is not None:
            object.__setattr__(self, "pricing_path", self.pricing_path.strip())
        if self.source is not None:
            object.__setattr__(self, "source", self.source.strip())
        object.__setattr__(self, "warnings", tuple(self.warnings))
        object.__setattr__(self, "notes", tuple(self.notes))
        if self.pv01 is None and self.dv01 is not None:
            object.__setattr__(self, "pv01", self.dv01)


@dataclass(frozen=True, slots=True)
class FutureQuoteOutput:
    """Future pricing output with futures price level semantics."""

    instrument_id: InstrumentId | None = None
    pricing_path: str | None = None
    futures_price: Decimal | None = None
    fair_value: Decimal | None = None
    implied_repo_rate: Decimal | None = None
    net_basis: Decimal | None = None
    gross_basis: Decimal | None = None
    conversion_factor: Decimal | None = None
    dv01: Decimal | None = None
    pv01: Decimal | None = None
    ctd_instrument_id: InstrumentId | None = None
    warnings: tuple[str, ...] = ()
    notes: tuple[str, ...] = ()
    settlement_date: Date | None = None
    timestamp: datetime | None = None
    source: str | None = None
    is_stale: bool = False
    quality_score: Decimal | None = None

    def __post_init__(self) -> None:
        if self.instrument_id is not None:
            object.__setattr__(self, "instrument_id", InstrumentId.parse(self.instrument_id))
        if self.ctd_instrument_id is not None:
            object.__setattr__(self, "ctd_instrument_id", InstrumentId.parse(self.ctd_instrument_id))
        _coerce_decimal_fields(
            self,
            "futures_price",
            "fair_value",
            "implied_repo_rate",
            "net_basis",
            "gross_basis",
            "conversion_factor",
            "dv01",
            "pv01",
            "quality_score",
        )
        if self.pricing_path is not None:
            object.__setattr__(self, "pricing_path", self.pricing_path.strip())
        if self.source is not None:
            object.__setattr__(self, "source", self.source.strip())
        object.__setattr__(self, "warnings", tuple(self.warnings))
        object.__setattr__(self, "notes", tuple(self.notes))
        if self.pv01 is None and self.dv01 is not None:
            object.__setattr__(self, "pv01", self.dv01)


@dataclass(frozen=True, slots=True)
class CdsQuoteOutput:
    """CDS pricing output with spread, upfront, and recovery fields."""

    instrument_id: InstrumentId | None = None
    pricing_path: str | None = None
    par_spread: Decimal | None = None
    upfront: Decimal | None = None
    present_value: Decimal | None = None
    cs01: Decimal | None = None
    pv01: Decimal | None = None
    risky_duration: Decimal | None = None
    recovery_rate: Decimal | None = None
    warnings: tuple[str, ...] = ()
    notes: tuple[str, ...] = ()
    settlement_date: Date | None = None
    timestamp: datetime | None = None
    source: str | None = None
    is_stale: bool = False
    quality_score: Decimal | None = None

    def __post_init__(self) -> None:
        if self.instrument_id is not None:
            object.__setattr__(self, "instrument_id", InstrumentId.parse(self.instrument_id))
        _coerce_decimal_fields(
            self,
            "par_spread",
            "upfront",
            "present_value",
            "cs01",
            "pv01",
            "risky_duration",
            "recovery_rate",
            "quality_score",
        )
        if self.pricing_path is not None:
            object.__setattr__(self, "pricing_path", self.pricing_path.strip())
        if self.source is not None:
            object.__setattr__(self, "source", self.source.strip())
        object.__setattr__(self, "warnings", tuple(self.warnings))
        object.__setattr__(self, "notes", tuple(self.notes))


@dataclass(frozen=True, slots=True)
class RvSignalOutput:
    """Relative-value signal output with normalized direction metadata."""

    instrument_id: InstrumentId | None = None
    signal_name: str | None = None
    score: Decimal | None = None
    z_score: Decimal | None = None
    direction: str | None = None
    bucket: str | None = None
    horizon: str | None = None
    timestamp: datetime | None = None
    source: str | None = None
    notes: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if self.instrument_id is not None:
            object.__setattr__(self, "instrument_id", InstrumentId.parse(self.instrument_id))
        _coerce_decimal_fields(self, "score", "z_score")
        if self.signal_name is not None:
            object.__setattr__(self, "signal_name", self.signal_name.strip())
        if self.direction is not None:
            object.__setattr__(self, "direction", self.direction.strip().lower())
        if self.bucket is not None:
            object.__setattr__(self, "bucket", self.bucket.strip())
        if self.horizon is not None:
            object.__setattr__(self, "horizon", self.horizon.strip())
        if self.source is not None:
            object.__setattr__(self, "source", self.source.strip())
        object.__setattr__(self, "notes", tuple(self.notes))


@dataclass(frozen=True, slots=True)
class EtfAnalyticsOutput:
    """ETF analytics output with NAV, price, and weighted exposure fields."""

    etf_id: EtfId | None = None
    gross_market_value: Decimal = Decimal(0)
    nav: Decimal | None = None
    inav: Decimal | None = None
    shares_outstanding: Decimal = Decimal(1)
    weighted_duration: Decimal | None = None
    weighted_convexity: Decimal | None = None
    aggregate_dv01: Decimal | None = None
    weighted_z_spread: Decimal | None = None
    weighted_g_spread: Decimal | None = None
    weighted_i_spread: Decimal | None = None
    holding_count: int = 0
    priced_count: int = 0
    fully_priced: bool = False
    settlement_date: Date | None = None
    timestamp: datetime | None = None
    source: str | None = None
    is_stale: bool = False
    quality_score: Decimal | None = None
    bid_price: Decimal | None = None
    mid_price: Decimal | None = None
    ask_price: Decimal | None = None
    pv01: Decimal | None = None
    cs01: Decimal | None = None

    def __post_init__(self) -> None:
        if self.etf_id is not None:
            object.__setattr__(self, "etf_id", EtfId.parse(self.etf_id))
        _coerce_decimal_fields(
            self,
            "gross_market_value",
            "nav",
            "inav",
            "shares_outstanding",
            "weighted_duration",
            "weighted_convexity",
            "aggregate_dv01",
            "weighted_z_spread",
            "weighted_g_spread",
            "weighted_i_spread",
            "quality_score",
            "bid_price",
            "mid_price",
            "ask_price",
            "pv01",
            "cs01",
        )
        if self.source is not None:
            object.__setattr__(self, "source", self.source.strip())
        if self.pv01 is None and self.aggregate_dv01 is not None:
            object.__setattr__(self, "pv01", self.aggregate_dv01)
        if self.mid_price is None and self.nav is not None:
            object.__setattr__(self, "mid_price", self.nav)


@dataclass(frozen=True, slots=True)
class PortfolioAnalyticsOutput:
    """Portfolio analytics output with aggregated market and risk fields."""

    portfolio_id: PortfolioId | None = None
    total_market_value: Decimal = Decimal(0)
    total_dirty_value: Decimal = Decimal(0)
    weighted_duration: Decimal | None = None
    weighted_convexity: Decimal | None = None
    aggregate_dv01: Decimal | None = None
    weighted_z_spread: Decimal | None = None
    weighted_g_spread: Decimal | None = None
    weighted_i_spread: Decimal | None = None
    key_rate_durations: dict[str, Decimal] = field(default_factory=dict)
    sector_breakdown: dict[str, Decimal] = field(default_factory=dict)
    rating_breakdown: dict[str, Decimal] = field(default_factory=dict)
    position_count: int = 0
    priced_count: int = 0
    fully_priced: bool = False
    settlement_date: Date | None = None
    timestamp: datetime | None = None
    source: str | None = None
    is_stale: bool = False
    quality_score: Decimal | None = None
    pv01: Decimal | None = None
    cs01: Decimal | None = None

    def __post_init__(self) -> None:
        if self.portfolio_id is not None:
            object.__setattr__(self, "portfolio_id", PortfolioId.parse(self.portfolio_id))
        _coerce_decimal_fields(
            self,
            "total_market_value",
            "total_dirty_value",
            "weighted_duration",
            "weighted_convexity",
            "aggregate_dv01",
            "weighted_z_spread",
            "weighted_g_spread",
            "weighted_i_spread",
            "quality_score",
            "pv01",
            "cs01",
        )
        object.__setattr__(
            self,
            "key_rate_durations",
            {key: _to_decimal(value) for key, value in self.key_rate_durations.items()},
        )
        object.__setattr__(
            self,
            "sector_breakdown",
            {key: _to_decimal(value) for key, value in self.sector_breakdown.items()},
        )
        object.__setattr__(
            self,
            "rating_breakdown",
            {key: _to_decimal(value) for key, value in self.rating_breakdown.items()},
        )
        if self.source is not None:
            object.__setattr__(self, "source", self.source.strip())
        if self.pv01 is None and self.aggregate_dv01 is not None:
            object.__setattr__(self, "pv01", self.aggregate_dv01)


AnalyticsOutput = (
    BondQuoteOutput
    | SwapQuoteOutput
    | BasisSwapQuoteOutput
    | FutureQuoteOutput
    | CdsQuoteOutput
    | RvSignalOutput
    | EtfAnalyticsOutput
    | PortfolioAnalyticsOutput
)


@runtime_checkable
class QuotePublisher(Protocol):
    """Protocol for publishing bond quote outputs."""

    def publish_quote(self, quote: BondQuoteOutput) -> None:
        ...


@runtime_checkable
class CurvePublisher(Protocol):
    """Protocol for publishing curve snapshots or inputs."""

    def publish_curve(self, curve_id: CurveId | str, curve: CurveInputs | CurveData) -> None:
        ...


@runtime_checkable
class EtfPublisher(Protocol):
    """Protocol for publishing ETF analytics outputs."""

    def publish_etf(self, analytics: EtfAnalyticsOutput) -> None:
        ...


@runtime_checkable
class AnalyticsPublisher(Protocol):
    """Protocol for publishing any supported analytics output."""

    def publish_analytics(self, analytics: AnalyticsOutput) -> None:
        ...


@runtime_checkable
class AlertPublisher(Protocol):
    """Protocol for publishing alert messages."""

    def publish_alert(self, message: str, *, severity: str = "info") -> None:
        ...


@dataclass(slots=True)
class OutputPublisher:
    """Composite publisher that delegates to optional channel publishers."""

    quote_publisher: QuotePublisher | None = None
    curve_publisher: CurvePublisher | None = None
    etf_publisher: EtfPublisher | None = None
    analytics_publisher: AnalyticsPublisher | None = None
    alert_publisher: AlertPublisher | None = None

    def publish_quote(self, quote: BondQuoteOutput) -> None:
        """Publish a bond quote when a quote publisher is configured."""
        if self.quote_publisher is not None:
            self.quote_publisher.publish_quote(quote)

    def publish_curve(self, curve_id: CurveId | str, curve: CurveInputs | CurveData) -> None:
        """Publish a curve payload when a curve publisher is configured."""
        if self.curve_publisher is not None:
            self.curve_publisher.publish_curve(curve_id, curve)

    def publish_etf(self, analytics: EtfAnalyticsOutput) -> None:
        """Publish ETF analytics when an ETF publisher is configured."""
        if self.etf_publisher is not None:
            self.etf_publisher.publish_etf(analytics)

    def publish_analytics(self, analytics: AnalyticsOutput) -> None:
        """Publish analytics or fall back to the specific channel handlers."""
        if self.analytics_publisher is not None:
            self.analytics_publisher.publish_analytics(analytics)
            return
        if isinstance(analytics, BondQuoteOutput):
            self.publish_quote(analytics)
        elif isinstance(analytics, EtfAnalyticsOutput):
            self.publish_etf(analytics)

    def publish_alert(self, message: str, *, severity: str = "info") -> None:
        """Publish an alert when an alert publisher is configured."""
        if self.alert_publisher is not None:
            self.alert_publisher.publish_alert(message, severity=severity)


__all__ = [
    "AlertPublisher",
    "AnalyticsOutput",
    "AnalyticsPublisher",
    "BasisSwapQuoteOutput",
    "BondQuoteOutput",
    "CdsQuoteOutput",
    "CurvePublisher",
    "EtfAnalyticsOutput",
    "EtfPublisher",
    "FutureQuoteOutput",
    "OutputPublisher",
    "PortfolioAnalyticsOutput",
    "QuotePublisher",
    "RvSignalOutput",
    "SwapQuoteOutput",
]
