"""
Optimizadores clásicos como baseline para comparación
Implementa métodos tradicionales de optimización de portafolio
"""

import numpy as np
import pandas as pd
from scipy.optimize import minimize
from typing import Dict, Optional
import cvxpy as cp


class MarkowitzOptimizer:
    """
    Optimizador de Markowitz (Mean-Variance)
    Modelo clásico de optimización de portafolio
    """
    
    def __init__(
        self,
        expected_returns: np.ndarray,
        cov_matrix: np.ndarray,
        risk_aversion: float = 0.5
    ):
        """
        Args:
            expected_returns: Retornos esperados de cada activo
            cov_matrix: Matriz de covarianza
            risk_aversion: Parámetro λ (0=solo retorno, 1=solo riesgo)
        """
        self.expected_returns = expected_returns
        self.cov_matrix = cov_matrix
        self.n_assets = len(expected_returns)
        self.risk_aversion = risk_aversion
    
    def optimize(
        self,
        max_weight: float = 0.30,
        min_weight: float = 0.0,
        target_return: Optional[float] = None
    ) -> Dict:
        """
        Optimiza el portafolio usando programación cuadrática
        
        Args:
            max_weight: Peso máximo por activo
            min_weight: Peso mínimo por activo
            target_return: Retorno objetivo opcional
            
        Returns:
            Dict con resultados
        """
        # Variables de decisión
        w = cp.Variable(self.n_assets)
        
        # Retorno del portafolio
        portfolio_return = self.expected_returns @ w
        
        # Riesgo del portafolio (varianza)
        portfolio_variance = cp.quad_form(w, self.cov_matrix)
        
        # Función objetivo: maximizar (retorno - λ*riesgo)
        # Equivalente a minimizar -(retorno - λ*riesgo)
        objective = cp.Minimize(-portfolio_return + self.risk_aversion * portfolio_variance)
        
        # Restricciones
        constraints = [
            cp.sum(w) == 1,  # Suma de pesos = 1
            w >= min_weight,  # Peso mínimo
            w <= max_weight   # Peso máximo
        ]
        
        # Si hay retorno objetivo
        if target_return is not None:
            constraints.append(portfolio_return >= target_return)
        
        # Resolver
        problem = cp.Problem(objective, constraints)
        problem.solve()
        
        if problem.status != 'optimal':
            print(f"⚠️  Warning: Optimización no alcanzó óptimo: {problem.status}")
        
        # Extraer resultados
        weights = w.value
        
        # Calcular métricas
        port_return = float(self.expected_returns @ weights)
        port_risk = float(np.sqrt(weights.T @ self.cov_matrix @ weights))
        sharpe = port_return / port_risk if port_risk > 0 else 0
        
        return {
            'weights': weights,
            'expected_return': port_return,
            'risk': port_risk,
            'sharpe_ratio': sharpe,
            'method': 'Markowitz Mean-Variance'
        }


class EqualWeightOptimizer:
    """Portafolio con pesos iguales (1/N)"""
    
    def __init__(self, n_assets: int):
        self.n_assets = n_assets
    
    def optimize(
        self,
        expected_returns: np.ndarray,
        cov_matrix: np.ndarray
    ) -> Dict:
        """Asigna peso igual a todos los activos"""
        weights = np.ones(self.n_assets) / self.n_assets
        
        port_return = float(expected_returns @ weights)
        port_risk = float(np.sqrt(weights.T @ cov_matrix @ weights))
        sharpe = port_return / port_risk if port_risk > 0 else 0
        
        return {
            'weights': weights,
            'expected_return': port_return,
            'risk': port_risk,
            'sharpe_ratio': sharpe,
            'method': 'Equal Weight (1/N)'
        }


class MinVarianceOptimizer:
    """Minimiza solo la varianza del portafolio"""
    
    def __init__(self, cov_matrix: np.ndarray):
        self.cov_matrix = cov_matrix
        self.n_assets = len(cov_matrix)
    
    def optimize(
        self,
        expected_returns: np.ndarray,
        max_weight: float = 0.30,
        min_weight: float = 0.0
    ) -> Dict:
        """Minimiza riesgo"""
        w = cp.Variable(self.n_assets)
        
        # Objetivo: minimizar varianza
        portfolio_variance = cp.quad_form(w, self.cov_matrix)
        objective = cp.Minimize(portfolio_variance)
        
        # Restricciones
        constraints = [
            cp.sum(w) == 1,
            w >= min_weight,
            w <= max_weight
        ]
        
        problem = cp.Problem(objective, constraints)
        problem.solve()
        
        weights = w.value
        
        port_return = float(expected_returns @ weights)
        port_risk = float(np.sqrt(weights.T @ self.cov_matrix @ weights))
        sharpe = port_return / port_risk if port_risk > 0 else 0
        
        return {
            'weights': weights,
            'expected_return': port_return,
            'risk': port_risk,
            'sharpe_ratio': sharpe,
            'method': 'Minimum Variance'
        }


class MaxSharpeOptimizer:
    """Maximiza el Sharpe Ratio"""
    
    def __init__(
        self,
        expected_returns: np.ndarray,
        cov_matrix: np.ndarray,
        risk_free_rate: float = 0.02
    ):
        self.expected_returns = expected_returns
        self.cov_matrix = cov_matrix
        self.n_assets = len(expected_returns)
        self.risk_free_rate = risk_free_rate
    
    def optimize(
        self,
        max_weight: float = 0.30,
        min_weight: float = 0.0
    ) -> Dict:
        """Maximiza Sharpe Ratio"""
        
        # Usar scipy optimize
        def negative_sharpe(weights):
            port_return = np.dot(weights, self.expected_returns)
            port_risk = np.sqrt(np.dot(weights.T, np.dot(self.cov_matrix, weights)))
            sharpe = (port_return - self.risk_free_rate) / port_risk
            return -sharpe  # Negativo porque minimize
        
        # Restricciones
        constraints = [
            {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}  # suma = 1
        ]
        
        # Bounds
        bounds = tuple((min_weight, max_weight) for _ in range(self.n_assets))
        
        # Punto inicial
        w0 = np.ones(self.n_assets) / self.n_assets
        
        # Optimizar
        result = minimize(
            negative_sharpe,
            w0,
            method='SLSQP',
            bounds=bounds,
            constraints=constraints
        )
        
        weights = result.x
        
        port_return = float(self.expected_returns @ weights)
        port_risk = float(np.sqrt(weights.T @ self.cov_matrix @ weights))
        sharpe = (port_return - self.risk_free_rate) / port_risk if port_risk > 0 else 0
        
        return {
            'weights': weights,
            'expected_return': port_return,
            'risk': port_risk,
            'sharpe_ratio': sharpe,
            'method': 'Maximum Sharpe Ratio'
        }


def compare_optimizers(
    expected_returns: np.ndarray,
    cov_matrix: np.ndarray,
    tickers: list,
    risk_aversion: float = 0.5
) -> pd.DataFrame:
    """
    Compara todos los métodos de optimización
    
    Returns:
        DataFrame con comparación de resultados
    """
    n_assets = len(expected_returns)
    results = []
    
    # Markowitz
    markowitz = MarkowitzOptimizer(expected_returns, cov_matrix, risk_aversion)
    res_mark = markowitz.optimize()
    results.append(res_mark)
    
    # Equal Weight
    equal_w = EqualWeightOptimizer(n_assets)
    res_equal = equal_w.optimize(expected_returns, cov_matrix)
    results.append(res_equal)
    
    # Min Variance
    min_var = MinVarianceOptimizer(cov_matrix)
    res_minvar = min_var.optimize(expected_returns)
    results.append(res_minvar)
    
    # Max Sharpe
    max_sharpe = MaxSharpeOptimizer(expected_returns, cov_matrix)
    res_sharpe = max_sharpe.optimize()
    results.append(res_sharpe)
    
    # Crear DataFrame comparativo
    comparison = pd.DataFrame({
        'Method': [r['method'] for r in results],
        'Expected Return': [r['expected_return'] for r in results],
        'Risk (Volatility)': [r['risk'] for r in results],
        'Sharpe Ratio': [r['sharpe_ratio'] for r in results]
    })
    
    # Agregar pesos
    for i, ticker in enumerate(tickers):
        comparison[ticker] = [r['weights'][i] for r in results]
    
    return comparison


if __name__ == '__main__':
    # Demo
    print("=" * 60)
    print("CLASSICAL OPTIMIZERS COMPARISON")
    print("=" * 60)
    
    # Datos sintéticos
    n_assets = 6
    tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'JPM', 'JNJ']
    
    expected_returns = np.array([0.12, 0.10, 0.14, 0.11, 0.08, 0.07])
    
    # Matriz de covarianza aleatoria
    np.random.seed(42)
    A = np.random.rand(n_assets, n_assets) * 0.1
    cov_matrix = (A + A.T) / 2 + np.eye(n_assets) * 0.05
    
    # Comparar métodos
    comparison = compare_optimizers(expected_returns, cov_matrix, tickers)
    
    print("\n", comparison.to_string(index=False))
    print(f"\n✓ Optimizadores clásicos funcionando!")
