"""
NGR Metrics Calculation Module

This module provides functions for calculating various error metrics
for NGR (Net Gaming Revenue) predictions with business-optimal weighting.
"""

from typing import Any, Dict, Optional

import numpy as np
import pandas as pd


def calculate_ngr_metrics(
    df: pd.DataFrame, late_stage_day: Optional[int] = None, late_stage_correction: Optional[float] = None
) -> Dict[str, Any]:
    """
    Calculate NGR metrics with business-optimal weighting.

    This function computes various error metrics (MAE, MAPE, ME, MPE) with business weights
    based on days_since_ftd. Optionally applies late-stage corrections to predictions.

    Parameters
    ----------
    df : pd.DataFrame
        DataFrame containing prediction results with columns:
        - 'days_since_ftd': int, days since first time deposit
        - 'remaining_ngr': float, actual remaining NGR
        - 'remaining_ngr_pred': float, predicted remaining NGR
    late_stage_day : int, optional
        Day threshold after which to apply correction
    late_stage_correction : float, optional
        Correction factor to apply (0.0-1.0) linearly after late_stage_day

    Returns
    -------
    dict
        Dictionary containing calculated metrics:
        - 'Standard MAE': Mean Absolute Error
        - 'Standard MAPE (%)': Mean Absolute Percentage Error
        - 'Standard ME': Mean Error
        - 'Standard MPE (%)': Mean Percentage Error
        - 'Business Optimal MAE': Weighted MAE
        - 'Business Optimal MAPE (%)': Weighted MAPE
        - 'Business Optimal ME': Weighted ME
        - 'Business Optimal MPE (%)': Weighted MPE
        - 'mean_abs_error_by_bin': DataFrame with errors by period
        - 'mean_values': DataFrame with aggregated values by day
        - 'weights': Array of business weights by day

    Notes
    -----
    Business-optimal weights by day ranges:
    - Days 1-7: 0.05 (learning period - insufficient data)
    - Days 8-14: 0.85 (early signals emerging)
    - Days 15-45: 1.00 (SUPER CRITICAL - optimal intervention window)
    - Days 46-90: 0.90 (confirmation period - high value)
    - Days 91-180: 0.60 (established patterns - moderate value)
    - Days 181-270: 0.40 (mature behavior - operational value)
    - Days 271+: 0.30 (end-game precision - tactical value)

    Examples
    --------
    >>> metrics = calculate_ngr_metrics(
    ...     df,
    ...     late_stage_day=320,
    ...     late_stage_correction=0.95
    ... )
    >>> print(f"Business MAE: {metrics['Business Optimal MAE']:.2f}")
    """

    # Calculate mean values grouped by days_since_ftd
    mean_values = (
        df.groupby("days_since_ftd")
        .agg(
            {
                "remaining_ngr": "mean",
                "remaining_ngr_pred": "mean",
            }
        )
        .reset_index()
    )
    mean_values = mean_values[mean_values["days_since_ftd"] < 365]

    # Apply late stage correction if specified
    if late_stage_day is not None and late_stage_correction is not None:
        days_array = mean_values["days_since_ftd"].values
        days_from_start = days_array - late_stage_day
        max_days = 364 - late_stage_day

        correction_factor = np.where(
            days_array <= late_stage_day,
            1.0,  # Before late_stage_day: no changes
            1.0 - (1.0 - late_stage_correction) * (days_from_start / max_days),  # Linear transition
        )
        mean_values["remaining_ngr_pred"] = mean_values["remaining_ngr_pred"] * correction_factor

    days = mean_values["days_since_ftd"].values.astype(np.float64)

    # Business-optimal weighting
    weights = np.where(
        days <= 7,
        0.05,  # Learning period - insufficient data
        np.where(
            days <= 14,
            0.85,  # Early signals emerging
            np.where(
                days <= 45,
                1.00,  # SUPER CRITICAL - optimal intervention window
                np.where(
                    days <= 90,
                    0.90,  # Confirmation period - high value
                    np.where(
                        days <= 180,
                        0.60,  # Established patterns - moderate value
                        np.where(days <= 270, 0.40, 0.30),  # Mature behavior - operational value
                    ),
                ),
            ),
        ),
    )  # End-game precision - tactical value

    abs_errors = np.abs(mean_values["remaining_ngr"] - mean_values["remaining_ngr_pred"])
    errors = mean_values["remaining_ngr"] - mean_values["remaining_ngr_pred"]

    # Define bins for error analysis by period
    bins = [0, 7, 14, 45, 90, 180, 270, 364]
    labels = ["1-7", "8-14", "15-45", "46-90", "91-180", "181-270", "271-364"]

    # Calculate standard metrics
    business_optimal_tmae = np.sum(abs_errors * weights) / np.sum(weights)
    business_optimal_me = np.sum(errors * weights) / np.sum(weights)
    standard_mae = round(np.mean(abs_errors), 4)
    standard_me = round(np.mean(errors), 4)

    # Calculate percentage-based metrics
    # Avoid division by zero - use small epsilon for very small values
    epsilon = 1e-6
    safe_remaining_ngr = np.where(np.abs(mean_values["remaining_ngr"]) < epsilon, epsilon, mean_values["remaining_ngr"])

    percentage_abs_errors = (
        np.abs((mean_values["remaining_ngr"] - mean_values["remaining_ngr_pred"]) / safe_remaining_ngr) * 100
    )
    errors_percentage = ((mean_values["remaining_ngr"] - mean_values["remaining_ngr_pred"]) / safe_remaining_ngr) * 100

    # Assign each day to a period bin
    day_bins = pd.cut(mean_values["days_since_ftd"], bins=bins, labels=labels, right=True, include_lowest=True)

    # Calculate mean errors by period
    mean_abs_error_by_bin = pd.DataFrame(
        {
            "bin": labels,
            "mean_abs_error": [abs_errors[day_bins == label].mean() for label in labels],
            "mean_error": [errors[day_bins == label].mean() for label in labels],
            "mape": [percentage_abs_errors[day_bins == label].mean() for label in labels],
        }
    )

    # Calculate business-optimal percentage metrics
    business_optimal_mape = np.sum(percentage_abs_errors * weights) / np.sum(weights)
    standard_mape = np.mean(percentage_abs_errors)

    business_optimal_mpe = np.sum(errors_percentage * weights) / np.sum(weights)
    standard_mpe = np.mean(errors_percentage)

    # Return dict with all metrics
    return {
        "Standard MAE": standard_mae,
        "Standard MAPE (%)": round(standard_mape, 1),
        "Standard ME": standard_me,
        "Standard MPE (%)": round(standard_mpe, 1),
        "Business Optimal MAE": round(business_optimal_tmae, 2),
        "Business Optimal MAPE (%)": round(business_optimal_mape, 1),
        "Business Optimal ME": round(business_optimal_me, 2),
        "Business Optimal MPE (%)": round(business_optimal_mpe, 1),
        "mean_abs_error_by_bin": mean_abs_error_by_bin,
        "mean_values": mean_values,
        "weights": weights,
    }
