"""
Métricas de evaluación de portafolios
"""

import numpy as np
import pandas as pd
from typing import Dict, Optional


def calculate_sharpe_ratio(
    returns: np.ndarray,
    risk_free_rate: float = 0.02,
    periods_per_year: int = 252
) -> float:
    """
    Calcula Sharpe Ratio
    
    Args:
        returns: Retornos del portafolio
        risk_free_rate: Tasa libre de riesgo anual
        periods_per_year: Periodos por año (252 para días, 12 para meses)
        
    Returns:
        Sharpe ratio anualizado
    """
    excess_returns = returns - (risk_free_rate / periods_per_year)
    
    if len(excess_returns) < 2:
        return 0.0
    
    return np.sqrt(periods_per_year) * np.mean(excess_returns) / np.std(excess_returns)


def calculate_sortino_ratio(
    returns: np.ndarray,
    risk_free_rate: float = 0.02,
    periods_per_year: int = 252
) -> float:
    """
    Calcula Sortino Ratio (como Sharpe pero solo penaliza volatilidad negativa)
    
    Args:
        returns: Retornos del portafolio
        risk_free_rate: Tasa libre de riesgo
        periods_per_year: Periodos por año
        
    Returns:
        Sortino ratio
    """
    excess_returns = returns - (risk_free_rate / periods_per_year)
    
    # Solo considerar retornos negativos para downside deviation
    downside_returns = excess_returns[excess_returns < 0]
    
    if len(downside_returns) < 2:
        return 0.0
    
    downside_std = np.std(downside_returns)
    
    if downside_std == 0:
        return 0.0
    
    return np.sqrt(periods_per_year) * np.mean(excess_returns) / downside_std


def calculate_max_drawdown(portfolio_values: np.ndarray) -> float:
    """
    Calcula el máximo drawdown
    
    Args:
        portfolio_values: Valores del portafolio a lo largo del tiempo
        
    Returns:
        Maximum drawdown (valor negativo)
    """
    cumulative_max = np.maximum.accumulate(portfolio_values)
    drawdowns = (portfolio_values - cumulative_max) / cumulative_max
    return np.min(drawdowns)


def calculate_var(
    returns: np.ndarray,
    confidence_level: float = 0.95
) -> float:
    """
    Calcula Value at Risk (VaR)
    
    Args:
        returns: Retornos del portafolio
        confidence_level: Nivel de confianza (default: 95%)
        
    Returns:
        VaR (valor negativo indica pérdida)
    """
    return np.percentile(returns, (1 - confidence_level) * 100)


def calculate_cvar(
    returns: np.ndarray,
    confidence_level: float = 0.95
) -> float:
    """
    Calcula Conditional Value at Risk (CVaR o Expected Shortfall)
    
    Args:
        returns: Retornos del portafolio
        confidence_level: Nivel de confianza
        
    Returns:
        CVaR (promedio de pérdidas peores que VaR)
    """
    var = calculate_var(returns, confidence_level)
    return np.mean(returns[returns <= var])


def calculate_portfolio_metrics(
    weights: np.ndarray,
    returns_data: pd.DataFrame,
    risk_free_rate: float = 0.02
) -> Dict:
    """
    Calcula todas las métricas principales de un portafolio
    
    Args:
        weights: Pesos del portafolio
        returns_data: DataFrame con retornos históricos de cada activo
        risk_free_rate: Tasa libre de riesgo
        
    Returns:
        Dict con todas las métricas
    """
    # Calcular retornos del portafolio
    portfolio_returns = returns_data.dot(weights)
    
    # Calcular valores del portafolio (comenzando con $1)
    portfolio_values = (1 + portfolio_returns).cumprod()
    
    # Métricas
    metrics = {
        # Retorno y Riesgo
        'total_return': float(portfolio_values.iloc[-1] - 1),
        'annualized_return': float(np.mean(portfolio_returns) * 252),
        'annualized_volatility': float(np.std(portfolio_returns) * np.sqrt(252)),
        
        # Ratios
        'sharpe_ratio': float(calculate_sharpe_ratio(portfolio_returns.values, risk_free_rate)),
        'sortino_ratio': float(calculate_sortino_ratio(portfolio_returns.values, risk_free_rate)),
        
        # Riesgo
        'max_drawdown': float(calculate_max_drawdown(portfolio_values.values)),
        'var_95': float(calculate_var(portfolio_returns.values, 0.95)),
        'cvar_95': float(calculate_cvar(portfolio_returns.values, 0.95)),
        
        # Otros
        'best_day': float(portfolio_returns.max()),
        'worst_day': float(portfolio_returns.min()),
        'positive_days': float((portfolio_returns > 0).sum() / len(portfolio_returns)),
    }
    
    return metrics


def compare_portfolios(
    portfolios: Dict[str, Dict],
    returns_data: pd.DataFrame
) -> pd.DataFrame:
    """
    Compara múltiples portafolios
    
    Args:
        portfolios: Dict {nombre: {'weights': array}}
        returns_data: DataFrame con retornos históricos
        
    Returns:
        DataFrame con comparación de métricas
    """
    comparison = []
    
    for name, portfolio in portfolios.items():
        weights = portfolio['weights']
        metrics = calculate_portfolio_metrics(weights, returns_data)
        metrics['Portfolio'] = name
        comparison.append(metrics)
    
    df = pd.DataFrame(comparison)
    
    # Reordenar columnas
    cols = ['Portfolio'] + [c for c in df.columns if c != 'Portfolio']
    df = df[cols]
    
    return df


if __name__ == '__main__':
    # Demo
    print("=" * 60)
    print("PORTFOLIO METRICS DEMO")
    print("=" * 60)
    
    # Datos sintéticos
    np.random.seed(42)
    n_days = 252
    n_assets = 4
    
    # Generar retornos aleatorios
    returns_data = pd.DataFrame(
        np.random.randn(n_days, n_assets) * 0.02 + 0.0003,
        columns=['AAPL', 'MSFT', 'GOOGL', 'AMZN']
    )
    
    # Dos portafolios de ejemplo
    portfolios = {
        'Equal Weight': {'weights': np.array([0.25, 0.25, 0.25, 0.25])},
        'Concentrated': {'weights': np.array([0.50, 0.30, 0.15, 0.05])}
    }
    
    # Comparar
    comparison = compare_portfolios(portfolios, returns_data)
    
    print("\n", comparison.to_string(index=False))
    print(f"\n✓ Métricas funcionando correctamente!")
