# """
# Financial evaluation metrics for TCGM.
# Provides:
#     - AUC
#     - Brier Score
#     - Expected Financial Loss
#     - Expected Monetary Loss (threshold optimizer)
# """

# import numpy as np
# from sklearn.metrics import roc_auc_score, brier_score_loss
# from .loss import financial_loss


# # -------------------------------------------------------------------------
# # AUC calculation (safe when only one class exists)
# # -------------------------------------------------------------------------
# def _safe_auc(y_true, y_prob):
#     y_true = np.asarray(y_true)
#     y_prob = np.asarray(y_prob)

#     # AUC is undefined if only one class exists
#     if len(np.unique(y_true)) < 2:
#         return float("nan")

#     return float(roc_auc_score(y_true, y_prob))


# # -------------------------------------------------------------------------
# # Brier Score
# # -------------------------------------------------------------------------
# def _brier(y_true, y_prob):
#     return float(brier_score_loss(y_true, y_prob))


# # -------------------------------------------------------------------------
# # Financial Expected Loss (model-implied asymmetric loss)
# # -------------------------------------------------------------------------
# def evaluate_financial_performance(y_true, y_prob, cost_fp=1.0, cost_fn=5.0):
#     """
#     Computes:
#         - AUC
#         - Brier Score
#         - Expected Loss using the financial asymmetric loss function
    
#     y_true: array-like of 0/1 labels
#     y_prob: predicted probabilities
#     """
#     y_true = np.asarray(y_true).astype(int)
#     y_prob = np.clip(np.asarray(y_prob), 0.0, 1.0)

#     auc = _safe_auc(y_true, y_prob)
#     brier = _brier(y_true, y_prob)

#     # Mean asymmetric loss across all samples
#     exp_loss = float(np.mean(financial_loss(y_true, y_prob, cost_fp, cost_fn)))

#     return {
#         "AUC": auc,
#         "Brier": brier,
#         "Expected_Loss": exp_loss,
#     }


# # -------------------------------------------------------------------------
# # Expected Monetary Loss (threshold sweep for decision optimization)
# # -------------------------------------------------------------------------
# def compute_expected_monetary_loss(
#     y_true,
#     y_prob,
#     exposure,
#     lgd=0.6,
#     cost_fp=50.0,
#     thresholds=None
# ):
#     """
#     Computes threshold-dependent monetary loss:
#         FP → operational cost
#         FN → exposure × LGD

#     Returns:
#         {
#             "best_threshold": float,
#             "best_loss": float,
#             "thresholds": array,
#             "loss_curve": array
#         }

#     y_true: binary ground truth (0/1)
#     y_prob: model predicted probabilities
#     exposure: monetary exposure per transaction
#     lgd: loss given default (fraction of exposure lost)
#     cost_fp: cost of wrongly blocking a legitimate transaction
#     thresholds: optional iterable of thresholds
#     """

#     y_true = np.asarray(y_true).astype(int)
#     y_prob = np.clip(np.asarray(y_prob), 0.0, 1.0)
#     exposure = np.asarray(exposure).astype(float)

#     if thresholds is None:
#         thresholds = np.linspace(0.01, 0.99, 99)

#     loss_values = []

#     best_loss = np.inf
#     best_threshold = None

#     for t in thresholds:
#         decisions = (y_prob >= t).astype(int)

#         # False Positives → blocking legitimate customers
#         fp_mask = (decisions == 1) & (y_true == 0)
#         fp_loss = fp_mask.sum() * cost_fp

#         # False Negatives → missed fraud → lose exposure × LGD
#         fn_mask = (decisions == 0) & (y_true == 1)
#         fn_loss = np.sum(fn_mask * exposure * lgd)

#         total_loss = fp_loss + fn_loss
#         loss_values.append(total_loss)

#         if total_loss < best_loss:
#             best_loss = total_loss
#             best_threshold = t

#     return {
#         "best_threshold": float(best_threshold),
#         "best_loss": float(best_loss),
#         "thresholds": np.array(thresholds),
#         "loss_curve": np.array(loss_values)
#     }


"""
Evaluation metrics for TCGM:
 - AUC (safe when one class present)
 - Brier score
 - evaluate_financial_performance (AUC, Brier, Expected_Loss)
 - compute_expected_monetary_loss (threshold sweep using exposure & LGD)
"""

from typing import Optional, Dict, Any, Sequence
import numpy as np
from sklearn.metrics import roc_auc_score, brier_score_loss
from .loss import financial_loss

ArrayLike = Sequence


def _safe_auc(y_true: ArrayLike, y_prob: ArrayLike) -> float:
    y_true = np.asarray(y_true)
    y_prob = np.asarray(y_prob)
    if len(np.unique(y_true)) < 2:
        return float("nan")
    return float(roc_auc_score(y_true, y_prob))


def _brier(y_true: ArrayLike, y_prob: ArrayLike) -> float:
    return float(brier_score_loss(y_true, y_prob))


def evaluate_financial_performance(y_true: ArrayLike, y_prob: ArrayLike, cost_fp: float = 1.0, cost_fn: float = 5.0) -> Dict[str, float]:
    """
    Returns a dict with:
      - AUC
      - Brier
      - Expected_Loss (mean of financial_loss)
    """
    y_true = np.asarray(y_true).astype(int)
    y_prob = np.clip(np.asarray(y_prob).astype(float), 0.0, 1.0)
    auc = _safe_auc(y_true, y_prob)
    brier = _brier(y_true, y_prob)
    exp_loss = float(np.mean(financial_loss(y_true, y_prob, cost_fp=cost_fp, cost_fn=cost_fn)))
    return {"AUC": auc, "Brier": brier, "Expected_Loss": exp_loss}


def compute_expected_monetary_loss(
    y_true: ArrayLike,
    y_prob: ArrayLike,
    exposure: ArrayLike,
    lgd: float = 0.6,
    cost_fp: float = 50.0,
    thresholds: Optional[ArrayLike] = None,
) -> Dict[str, Any]:
    """
    Sweep thresholds and compute monetary loss:
      - FP cost = cost_fp per false positive
      - FN cost = exposure * lgd for each missed positive

    Returns dictionary:
      { "best_threshold", "best_loss", "thresholds", "loss_curve" }
    """
    y_true = np.asarray(y_true).astype(int)
    y_prob = np.clip(np.asarray(y_prob).astype(float), 0.0, 1.0)
    exposure = np.asarray(exposure).astype(float)

    if thresholds is None:
        thresholds = np.linspace(0.01, 0.99, 99)

    loss_values = []
    best_loss = float("inf")
    best_t = None

    for t in thresholds:
        preds = (y_prob >= t).astype(int)
        # FP: predicted 1 while true 0
        fp_count = int(((preds == 1) & (y_true == 0)).sum())
        fp_cost = fp_count * cost_fp
        # FN: predicted 0 while true 1 -> exposure * lgd for those samples
        fn_mask = (preds == 0) & (y_true == 1)
        fn_cost = float(np.sum(exposure[fn_mask] * lgd)) if np.any(fn_mask) else 0.0
        total = fp_cost + fn_cost
        loss_values.append(total)
        if total < best_loss:
            best_loss = total
            best_t = float(t)

    return {
        "best_threshold": best_t,
        "best_loss": float(best_loss),
        "thresholds": np.asarray(thresholds, dtype=float),
        "loss_curve": np.asarray(loss_values, dtype=float),
    }