import numpy as np
from scipy import stats


def cvar_estimator(vec: np.ndarray, alpha: float) -> float:
    """
    Calculate the Conditional Value at Risk (CVaR) for a given vector of values.
    CVaR is the expected value of the worst (1-alpha)% of cases, where "worst"
    means the highest values (assuming these represent costs or risks).

    Args:
        vec: Array of values
        alpha: Confidence level (between 0 and 1)

    Returns:
        float: The CVaR value

    Raises:
        ValueError: If alpha is not between 0 and 1 or if vec is empty
    """
    if not 0 <= alpha <= 1:
        raise ValueError("alpha must be between 0 and 1")
    if len(vec) == 0:
        raise ValueError("Input vector must not be empty")

    if len(vec) == 1:
        return float(vec[0])

    sorted_vec = np.sort(vec)
    n = len(sorted_vec)

    if alpha == 1.0:
        return float(sorted_vec[-1])
    if alpha == 0.0:
        return float(sorted_vec[0])

    n = len(sorted_vec)  # Get the number of elements

    # Create indices and weights vectorized
    indices = np.arange(1, n)  # 1 to n-1
    weights = np.maximum(0, (indices / n) - (1 - alpha))

    # Calculate differences vectorized
    diffs = sorted_vec[1:] - sorted_vec[:-1]

    # Calculate final result vectorized
    s = sorted_vec[-1] - np.sum(weights * diffs) / alpha

    return s


def cvar_bound_const_eps(y_samp: np.ndarray, y_sup: float, y_inf: float, x_sup: float, x_inf: float, eps: float, alpha: float) -> tuple[float, float]:
    """
    Calculate bounds for CVaR using a constant epsilon parameter.
    
    Args:
        y_samp: Array of sample values
        y_sup: Upper bound of the distribution
        y_inf: Lower bound of the distribution
        eps: Epsilon parameter for the bound calculation
        alpha: Confidence level (default 0.05)
        
    Returns:
        tuple: (lower_bound, upper_bound) representing the bounds for CVaR
        
    Raises:
        ValueError: If alpha or eps are not between 0 and 1, or if y_samp is empty
    """
    if not 0 <= alpha <= 1:
        raise ValueError("alpha must be between 0 and 1")
    if not 0 <= eps <= 1:
        raise ValueError("eps must be between 0 and 1")
    if len(y_samp) == 0:
        raise ValueError("Input vector must not be empty")
       
    inf = min(x_inf, y_inf)
    sup = max(x_sup, y_sup)
    # Calculate lower bound
    if eps + alpha < 1:
        lower_bound = (alpha + eps) / alpha * cvar_estimator(y_samp, alpha + eps) - eps / alpha * cvar_estimator(y_samp, eps)
    else:
        y_samp_mean = np.mean(y_samp)
        lower_bound = (alpha + eps - 1) * inf + y_samp_mean - eps * cvar_estimator(y_samp, eps)
        lower_bound /= alpha
        
    # Calculate upper bound
    if eps < alpha:
        upper_bound = (alpha - eps) / alpha * cvar_estimator(y_samp, alpha - eps) + eps / alpha * sup
    else:
        upper_bound = sup
        
    return lower_bound, upper_bound

def stochastic_cvar_bound_const_eps(y_samp: np.ndarray, y_sup: float, y_inf: float, x_sup: float, x_inf: float, eps: float, delta: float, alpha: float) -> tuple[float, float]:
    """
    Calculate bounds for CVaR using a stochastic epsilon parameter.
    """
    if not 0 <= alpha <= 1:
        raise ValueError("alpha must be between 0 and 1")
    if len(y_samp) == 0:
        raise ValueError("Input vector must not be empty")
    
    n_samples = len(y_samp)
    eta = np.sqrt(np.log(1/delta) / (2 * n_samples))
    eps_tag = eps + eta
    
    return cvar_bound_const_eps(y_samp=y_samp, y_sup=y_sup, y_inf=y_inf, x_sup=x_sup, x_inf=x_inf, eps=eps_tag, alpha=alpha)

def cvar_interpretable_concentration_inequality(y_samp: np.ndarray, y_sup: float, y_inf: float, delta: float, alpha: float) -> float:
    eps = 0.
    x_sup = y_sup
    x_inf = y_inf
    
    return stochastic_cvar_bound_const_eps(y_samp=y_samp, y_sup=y_sup, y_inf=y_inf, x_sup=x_sup, x_inf=x_inf, eps=eps, delta=delta, alpha=alpha)

def cvar_probabilistic_lower_bound_thomas(vec: np.ndarray, alpha: float, delta: float, dist_lower_bound: float) -> float:
    """
    Calculate a probabilistic lower bound for CVaR using Thomas's method.
    
    Args:
        vec: Array of values
        alpha: Confidence level (between 0 and 1)
        delta: Probability of the bound holding (between 0 and 1)
        dist_lower_bound: Lower bound of the distribution
        
    Returns:
        float: The probabilistic lower bound for CVaR
        
    Raises:
        ValueError: If alpha or delta are not between 0 and 1, or if vec is empty
    """
    if not 0 <= alpha <= 1:
        raise ValueError("alpha must be between 0 and 1")
    if not 0 <= delta <= 1:
        raise ValueError("delta must be between 0 and 1")
    if len(vec) == 0:
        raise ValueError("Input vector must not be empty")
    
    sorted_vec = np.sort(vec)
    n = len(sorted_vec)
    
    # Calculate weights vectorized
    indices = np.arange(n)  # 0 to n-1
    weights = np.maximum(0, np.minimum(1, (indices / n) + np.sqrt(np.log(1/delta) / (2 * n))) - (1 - alpha))
    
    # Calculate differences vectorized
    diffs = sorted_vec[1:] - sorted_vec[:-1]
    
    # Calculate final result vectorized
    s = sorted_vec[-1] - np.sum(weights[1:] * diffs) / alpha
    
    # Add the lower bound term
    s -= weights[0] * (sorted_vec[0] - dist_lower_bound) / alpha
    
    return s


def cvar_probabilistic_upper_bound_thomas(vec: np.ndarray, alpha: float, delta: float, dist_upper_bound: float) -> float:
    """
    Calculate a probabilistic upper bound for CVaR using Thomas's method.
    
    Args:
        vec: Array of values
        alpha: Confidence level (between 0 and 1)
        delta: Probability of the bound holding (between 0 and 1)
        dist_upper_bound: Upper bound of the distribution
        
    Returns:
        float: The probabilistic upper bound for CVaR
        
    Raises:
        ValueError: If alpha or delta are not between 0 and 1, or if vec is empty
    """
    if not 0 <= alpha <= 1:
        raise ValueError("alpha must be between 0 and 1")
    if not 0 <= delta <= 1:
        raise ValueError("delta must be between 0 and 1")
    if len(vec) == 0:
        raise ValueError("Input vector must not be empty")
        
    sorted_vec = np.sort(vec)
    n = len(sorted_vec)
    
    # Calculate weights vectorized
    indices = np.arange(1, n + 1)  # 1 to n
    weights = np.maximum(0, (indices / n) - np.sqrt(np.log(1/delta) / (2 * n)) - (1 - alpha))
    
    # Calculate differences vectorized
    diffs = sorted_vec[1:] - sorted_vec[:-1]
    
    # Calculate initial term
    initial_weight = weights[-1]
    s = dist_upper_bound - (dist_upper_bound - sorted_vec[-1]) * initial_weight / alpha
    
    # Calculate final result vectorized
    s -= np.sum(weights[:-1] * diffs) / alpha
    
    return s