"""CECL (Current Expected Credit Losses) lifetime loss engine.

Uses CausalPDModel for forward PD estimation and soliton scores for
dynamic forecast horizons per ADR-011.
"""

from __future__ import annotations

from dataclasses import dataclass

from orxaq.credit.causal_pd import CausalPDModel
from orxaq.kernel import WorldDAG


@dataclass(frozen=True)
class ECLResult:
    """Result of expected credit loss calculation."""

    total_ecl: float
    ecl_by_segment: dict[str, float]
    horizon_quarters: int
    horizon_method: str  # "soliton_dynamic" or "fixed"
    confidence_band: tuple[float, float]  # (low, high)
    pd_mean: float
    lgd: float
    ead_total: float


# Default LGD for Tier 0 (Basel II foundation approach)
DEFAULT_LGD = 0.45

# Soliton thresholds for dynamic horizon (ADR-011)
_SOLITON_HIGH = 0.80
_SOLITON_MEDIUM = 0.60


class CECLEngine:
    """CECL lifetime expected credit loss calculator.

    Combines CausalPDModel for PD estimation with soliton stability
    scores for dynamic forecast horizon selection.
    """

    def __init__(
        self,
        pd_model: CausalPDModel,
        dag: WorldDAG,
        lgd: float = DEFAULT_LGD,
    ) -> None:
        self._pd_model = pd_model
        self._dag = dag
        self._lgd = lgd

    def expected_credit_loss(
        self,
        portfolio: list[dict[str, float]],
        horizon_quarters: int | None = None,
    ) -> ECLResult:
        """Calculate CECL expected credit loss for a portfolio.

        ECL = sum over loans of PD * LGD * EAD, projected over the
        forecast horizon. Uses soliton-dynamic horizons if horizon
        not explicitly specified.

        Args:
            portfolio: List of borrower dicts with credit variables.
            horizon_quarters: Override horizon (None = dynamic).
        """
        if not self._pd_model.is_fitted:
            msg = "PD model not fitted. Call pd_model.fit() first."
            raise RuntimeError(msg)

        if not portfolio:
            return ECLResult(
                total_ecl=0.0,
                ecl_by_segment={},
                horizon_quarters=0,
                horizon_method="fixed",
                confidence_band=(0.0, 0.0),
                pd_mean=0.0,
                lgd=self._lgd,
                ead_total=0.0,
            )

        if horizon_quarters is None:
            horizon_quarters = self.dynamic_horizon()
            horizon_method = "soliton_dynamic"
        else:
            horizon_method = "fixed"

        # Calculate ECL per loan
        total_ecl = 0.0
        ecl_by_segment: dict[str, float] = {}
        pd_sum = 0.0

        for borrower in portfolio:
            pd = self._pd_model.predict_pd(borrower)
            ead = borrower.get("current_balance", borrower.get("loan_amount", 0.0))

            # Project PD over horizon with time decay
            # PD increases with horizon as uncertainty grows
            cumulative_pd = 1.0 - (1.0 - pd) ** (horizon_quarters / 4.0)
            loan_ecl = cumulative_pd * self._lgd * ead

            total_ecl += loan_ecl
            pd_sum += pd

            # Segment by domain (could be expanded to custom segmentation)
            domain = borrower.get("_segment", "unsegmented")
            segment_key = str(domain)
            ecl_by_segment[segment_key] = ecl_by_segment.get(segment_key, 0.0) + loan_ecl

        pd_mean = pd_sum / len(portfolio)
        ead_total = sum(b.get("current_balance", b.get("loan_amount", 0.0)) for b in portfolio)

        # Confidence band based on horizon method
        if horizon_method == "soliton_dynamic":
            avg_soliton = self._average_soliton_score()
            band_width = (1.0 - avg_soliton) * total_ecl * 0.5
        else:
            band_width = total_ecl * 0.25  # Fixed 25% band

        confidence_band = (
            max(0.0, total_ecl - band_width),
            total_ecl + band_width,
        )

        return ECLResult(
            total_ecl=total_ecl,
            ecl_by_segment=ecl_by_segment,
            horizon_quarters=horizon_quarters,
            horizon_method=horizon_method,
            confidence_band=confidence_band,
            pd_mean=pd_mean,
            lgd=self._lgd,
            ead_total=ead_total,
        )

    def dynamic_horizon(self) -> int:
        """Determine forecast horizon from soliton stability scores.

        Per ADR-011:
        - Strong soliton (>0.80): 12 quarters (3 years)
        - Moderate (0.60-0.80): 8 quarters (2 years)
        - Weak (<0.60): 4 quarters (1 year)
        """
        avg_score = self._average_soliton_score()

        if avg_score >= _SOLITON_HIGH:
            return 12
        if avg_score >= _SOLITON_MEDIUM:
            return 8
        return 4

    def _average_soliton_score(self) -> float:
        """Average soliton score across key causal paths to default."""
        # Key paths: macro → borrower → default
        key_paths = self._dag.all_directed_paths("unemployment_rate", "default_flag")
        if not key_paths:
            return 0.5  # Default moderate confidence

        scores = []
        for path in key_paths:
            for i in range(len(path) - 1):
                source, target = path[i], path[i + 1]
                for edge in self._get_edges(source, target):
                    scores.append(edge.soliton_score)

        if not scores:
            return 0.5

        return float(sum(scores) / len(scores))

    def _get_edges(self, source: str, target: str) -> list:
        """Get edges between source and target from the DAG."""
        # WorldDAG stores edges as dict[str, CausalEdge] keyed by "source→target"
        edge_id = f"{source}\u2192{target}"
        edge = self._dag._edges.get(edge_id)  # noqa: SLF001
        return [edge] if edge else []
