"""Credit risk pipeline — declarative step execution with audit trail.

Chains credit operations (PD, ECL, stress, fairness, portfolio, report)
into a reusable pipeline with per-step timing and error capture.

Stdlib-only per ADR-007.
"""

from __future__ import annotations

import time
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class StepStatus(Enum):
    """Pipeline step execution status."""

    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    SKIPPED = "skipped"


@dataclass
class StepResult:
    """Result of a single pipeline step."""

    name: str
    status: StepStatus
    duration_ms: float = 0.0
    output: object = None
    error: str = ""

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "status": self.status.value,
            "duration_ms": round(self.duration_ms, 2),
            "error": self.error,
        }


@dataclass
class PipelineResult:
    """Result of a full pipeline execution."""

    steps: list[StepResult] = field(default_factory=list)
    total_duration_ms: float = 0.0
    success: bool = True

    def to_dict(self) -> dict:
        return {
            "success": self.success,
            "total_duration_ms": round(self.total_duration_ms, 2),
            "steps": [s.to_dict() for s in self.steps],
        }


class CreditPipeline:
    """Declarative credit risk pipeline.

    Usage::

        result = (
            CreditPipeline(engine)
            .load_ontology()
            .generate_data(n=200)
            .fit_pd()
            .compute_ecl()
            .stress_test("adverse")
            .portfolio_analysis()
            .fairness_audit("age")
            .generate_report()
            .run()
        )
    """

    def __init__(self, engine: Any) -> None:
        self._engine = engine
        self._steps: list[tuple[str, Callable[..., Any]]] = []
        self._context: dict[str, Any] = {}

    def load_ontology(self) -> CreditPipeline:
        """Step: Load credit ontology into the DAG."""

        def _run(ctx: dict) -> object:
            dag = self._engine.load_credit_ontology()
            ctx["dag"] = dag
            return {"nodes": len(dag.entities), "edges": len(dag.edges)}

        self._steps.append(("load_ontology", _run))
        return self

    def generate_data(self, n: int = 200, seed: int = 42) -> CreditPipeline:
        """Step: Generate synthetic credit data."""

        def _run(ctx: dict) -> object:
            if not self._engine.world_dag.entities:
                self._engine.load_credit_ontology()
            data = self._engine.generate_synthetic(n, seed=seed)
            ctx["data"] = data
            return {"rows": len(data)}

        self._steps.append(("generate_data", _run))
        return self

    def fit_pd(self) -> CreditPipeline:
        """Step: Fit causal PD model."""

        def _run(ctx: dict) -> object:
            data = ctx.get("data")
            if data is None:
                msg = "No data available — call generate_data() or load data first"
                raise ValueError(msg)
            model = self._engine.fit_pd_model(data)
            ctx["model"] = model
            avg_pd = sum(model.predict_pd(row) for row in data) / len(data)
            return {"avg_pd": round(avg_pd, 6)}

        self._steps.append(("fit_pd", _run))
        return self

    def compute_ecl(self) -> CreditPipeline:
        """Step: Compute CECL expected credit loss."""

        def _run(ctx: dict) -> object:
            data = ctx.get("data")
            if data is None:
                msg = "No data available"
                raise ValueError(msg)
            ecl = self._engine.compute_ecl(data)
            ctx["ecl"] = ecl
            return {"total_ecl": ecl.total_ecl, "horizon": ecl.horizon_quarters}

        self._steps.append(("compute_ecl", _run))
        return self

    def stress_test(self, scenario: str = "adverse") -> CreditPipeline:
        """Step: Run stress test under specified scenario."""

        def _run(ctx: dict) -> object:
            from orxaq.credit.portfolio import LoanExposure, PortfolioAnalyzer
            from orxaq.credit.scenarios import ADVERSE, BASELINE, SEVERELY_ADVERSE

            data = ctx.get("data")
            model = ctx.get("model")
            if data is None or model is None:
                msg = "No data/model — call generate_data() and fit_pd() first"
                raise ValueError(msg)

            scenarios = {
                "baseline": BASELINE,
                "adverse": ADVERSE,
                "severely_adverse": SEVERELY_ADVERSE,
            }
            sc = scenarios.get(scenario, ADVERSE)

            base_pds = [model.predict_pd(row) for row in data]
            stressed_pds = []
            for row in data:
                pds = [model.interventional_pd(row, var, val) for var, val in sc.items()]
                stressed_pds.append(sum(pds) / len(pds) if pds else model.predict_pd(row))

            def _make(pd_list):
                return [
                    LoanExposure(
                        loan_id=str(i),
                        pd=pd_list[i],
                        lgd=float(data[i].get("lgd", 0.45)),
                        ead=float(data[i].get("ead", data[i].get("loan_amount", 0.0))),
                        sector=str(data[i].get("sector", "unknown")),
                        vintage=str(data[i].get("vintage", "unknown")),
                    )
                    for i in range(len(pd_list))
                ]

            base_m = PortfolioAnalyzer(_make(base_pds)).metrics()
            stress_m = PortfolioAnalyzer(_make(stressed_pds)).metrics()
            ctx["stress"] = {
                "scenario": scenario,
                "base_el": base_m.expected_loss,
                "stressed_el": stress_m.expected_loss,
                "base_ec": base_m.economic_capital,
                "stressed_ec": stress_m.economic_capital,
            }
            return ctx["stress"]

        self._steps.append(("stress_test", _run))
        return self

    def portfolio_analysis(self) -> CreditPipeline:
        """Step: Run portfolio analytics."""

        def _run(ctx: dict) -> object:
            from orxaq.credit.portfolio import LoanExposure, PortfolioAnalyzer

            data = ctx.get("data")
            model = ctx.get("model")
            if data is None or model is None:
                msg = "No data/model available"
                raise ValueError(msg)

            exposures = [
                LoanExposure(
                    loan_id=str(i),
                    pd=model.predict_pd(row),
                    lgd=float(row.get("lgd", 0.45)),
                    ead=float(row.get("ead", row.get("loan_amount", 0.0))),
                    sector=str(row.get("sector", "unknown")),
                    vintage=str(row.get("vintage", "unknown")),
                )
                for i, row in enumerate(data)
            ]
            pm = PortfolioAnalyzer(exposures).metrics()
            ctx["portfolio"] = pm
            return {
                "expected_loss": pm.expected_loss,
                "economic_capital": pm.economic_capital,
                "hhi": pm.hhi,
                "count": pm.count,
            }

        self._steps.append(("portfolio_analysis", _run))
        return self

    def fairness_audit(self, protected: str = "age") -> CreditPipeline:
        """Step: Run fair lending analysis."""

        def _run(ctx: dict) -> object:
            decomp = self._engine.fairness_audit(protected=protected)
            ctx["fairness"] = decomp
            return {
                "total_effect": decomp.total_effect,
                "direct_effect": decomp.direct_effect,
                "concerning_paths": len(decomp.concerning_paths),
            }

        self._steps.append(("fairness_audit", _run))
        return self

    def generate_report(self) -> CreditPipeline:
        """Step: Generate unified risk report from collected results."""

        def _run(ctx: dict) -> object:
            from orxaq.credit.reporting import RiskReportBuilder

            builder = RiskReportBuilder()

            pm = ctx.get("portfolio")
            if pm is not None:
                builder.add_portfolio_section(
                    expected_loss=pm.expected_loss,
                    unexpected_loss=pm.unexpected_loss,
                    economic_capital=pm.economic_capital,
                    hhi=pm.hhi,
                    gini=pm.gini,
                    count=pm.count,
                    total_ead=pm.total_ead,
                )

            ecl = ctx.get("ecl")
            if ecl is not None:
                builder.add_ecl_section(
                    total_ecl=ecl.total_ecl,
                    horizon_quarters=ecl.horizon_quarters,
                    horizon_method=ecl.horizon_method,
                    pd_mean=ecl.pd_mean,
                    lgd=ecl.lgd,
                    confidence_low=ecl.confidence_band[0],
                    confidence_high=ecl.confidence_band[1],
                )

            stress = ctx.get("stress")
            if stress is not None:
                builder.add_stress_section(
                    scenario_name=stress["scenario"],
                    base_el=stress["base_el"],
                    stressed_el=stress["stressed_el"],
                    base_ec=stress["base_ec"],
                    stressed_ec=stress["stressed_ec"],
                )

            fairness = ctx.get("fairness")
            if fairness is not None:
                builder.add_fairness_section(
                    protected_var=fairness.protected_var,
                    total_effect=fairness.total_effect,
                    direct_effect=fairness.direct_effect,
                    indirect_paths=len(fairness.indirect_effects),
                    concerning_paths=len(fairness.concerning_paths),
                )

            report = builder.build()
            ctx["report"] = report
            return {
                "risk_rating": report.risk_rating,
                "sections": len(report.sections),
                "report_id": report.report_id,
            }

        self._steps.append(("generate_report", _run))
        return self

    def run(self) -> PipelineResult:
        """Execute all registered steps sequentially."""
        result = PipelineResult()
        start = time.monotonic()

        for name, fn in self._steps:
            step_start = time.monotonic()
            step = StepResult(name=name, status=StepStatus.RUNNING)
            try:
                output = fn(self._context)
                step.status = StepStatus.COMPLETED
                step.output = output
            except Exception as exc:  # noqa: BLE001
                step.status = StepStatus.FAILED
                step.error = str(exc)
                result.success = False
            step.duration_ms = (time.monotonic() - step_start) * 1000
            result.steps.append(step)

            if step.status == StepStatus.FAILED:
                # Skip remaining steps
                for remaining_name, _ in self._steps[len(result.steps) :]:
                    result.steps.append(StepResult(name=remaining_name, status=StepStatus.SKIPPED))
                break

        result.total_duration_ms = (time.monotonic() - start) * 1000
        return result
