"""Regression helpers for fitted bond curves.

Regression exposures are additive adjustments in dirty-price space. Metadata
fields can be mapped into numeric exposures such as amount outstanding,
liquidity, benchmark flags, and futures-deliverable indicators.
"""

from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
from typing import Iterable

import numpy as np
from numpy.typing import NDArray

from fuggers_py.data.reference_data import BondReferenceData


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


def _normalize_name(name: str) -> str:
    normalized = name.strip().lower().replace("-", "_").replace(" ", "_")
    if not normalized:
        raise ValueError("Regression exposure names must be non-empty.")
    return normalized


@dataclass(frozen=True, slots=True)
class RegressionExposure:
    """Single additive regression exposure."""

    name: str
    value: Decimal

    def __post_init__(self) -> None:
        object.__setattr__(self, "name", _normalize_name(self.name))
        object.__setattr__(self, "value", _to_decimal(self.value))


@dataclass(frozen=True, slots=True)
class RegressionDesignMatrix:
    """Dense regression design matrix for fitted-bond adjustments."""

    variable_names: tuple[str, ...]
    rows: tuple[tuple[Decimal, ...], ...]

    def as_numpy(self) -> NDArray[np.float64]:
        """Return the design matrix as a NumPy array."""
        if not self.variable_names:
            return np.zeros((len(self.rows), 0), dtype=float)
        return np.asarray([[float(value) for value in row] for row in self.rows], dtype=float)


def metadata_regression_exposures(
    reference_data: BondReferenceData | None,
    *,
    fields: Iterable[str] = (),
) -> tuple[RegressionExposure, ...]:
    """Map bond reference data fields to regression exposures."""
    if reference_data is None:
        return ()
    exposures: list[RegressionExposure] = []
    for raw_field in fields:
        field = _normalize_name(raw_field)
        if field == "amount_outstanding" and reference_data.amount_outstanding is not None:
            exposures.append(RegressionExposure(field, reference_data.amount_outstanding))
            continue
        if field == "liquidity_score" and reference_data.liquidity_score is not None:
            exposures.append(RegressionExposure(field, reference_data.liquidity_score))
            continue
        if field == "benchmark_flag":
            exposures.append(RegressionExposure(field, Decimal(1) if reference_data.benchmark_flag else Decimal(0)))
            continue
        if field == "futures_deliverable":
            exposures.append(
                RegressionExposure(
                    field,
                    Decimal(1) if reference_data.futures_deliverable_flags else Decimal(0),
                )
            )
            continue
        if field == "futures_deliverable_count":
            exposures.append(RegressionExposure(field, Decimal(len(reference_data.futures_deliverable_flags))))
            continue
        if field.startswith("futures_deliverable:"):
            flag = field.split(":", 1)[1].strip().upper()
            exposures.append(
                RegressionExposure(
                    field,
                    Decimal(1) if flag in reference_data.futures_deliverable_flags else Decimal(0),
                )
            )
    return tuple(exposures)


def merge_regression_exposures(*groups: Iterable[RegressionExposure]) -> tuple[RegressionExposure, ...]:
    """Merge exposure groups using last-write-wins semantics."""
    merged: dict[str, Decimal] = {}
    for group in groups:
        for exposure in group:
            merged[exposure.name] = exposure.value
    return tuple(RegressionExposure(name, value) for name, value in merged.items())


def build_design_matrix(
    observations: Iterable[object],
    *,
    variable_names: Iterable[str] | None = None,
    metadata_fields: Iterable[str] = (),
) -> RegressionDesignMatrix:
    """Build a dense design matrix from bond observations."""
    normalized_metadata_fields = tuple(_normalize_name(field) for field in metadata_fields)
    exposures_by_observation: list[tuple[RegressionExposure, ...]] = []
    inferred_names: set[str] = set()

    for observation in observations:
        base_exposures = tuple(getattr(observation, "regression_exposures", ()))
        reference_data = getattr(observation, "reference_data", None)
        combined = merge_regression_exposures(
            base_exposures,
            metadata_regression_exposures(reference_data, fields=normalized_metadata_fields),
        )
        exposures_by_observation.append(combined)
        inferred_names.update(exposure.name for exposure in combined)

    if variable_names is None:
        ordered_names = tuple(sorted(inferred_names))
    else:
        ordered_names = tuple(_normalize_name(name) for name in variable_names)

    rows: list[tuple[Decimal, ...]] = []
    for combined in exposures_by_observation:
        exposure_map = {exposure.name: exposure.value for exposure in combined}
        rows.append(tuple(exposure_map.get(name, Decimal(0)) for name in ordered_names))
    return RegressionDesignMatrix(variable_names=ordered_names, rows=tuple(rows))


def evaluate_regression_adjustment(
    exposures: Iterable[RegressionExposure],
    *,
    variable_names: tuple[str, ...],
    coefficients: tuple[Decimal, ...],
    reference_data: BondReferenceData | None = None,
    metadata_fields: Iterable[str] = (),
) -> Decimal:
    """Evaluate the additive regression adjustment for a bond."""
    combined = merge_regression_exposures(
        exposures,
        metadata_regression_exposures(reference_data, fields=metadata_fields),
    )
    exposure_map = {exposure.name: exposure.value for exposure in combined}
    total = Decimal(0)
    for name, coefficient in zip(variable_names, coefficients, strict=True):
        total += coefficient * exposure_map.get(name, Decimal(0))
    return total


__all__ = [
    "RegressionDesignMatrix",
    "RegressionExposure",
    "build_design_matrix",
    "evaluate_regression_adjustment",
    "merge_regression_exposures",
    "metadata_regression_exposures",
]
