"""
Gestionnaire de corrélation BTC pour pondération des entrées de trading
"""

import numpy as np
import pandas as pd
from typing import Dict, List, Optional, Tuple
from datetime import datetime, timedelta
import logging
from collections import deque
import threading
import time

class BTCCorrelationManager:
    """
    Calcule et gère les corrélations entre BTC et autres cryptos
    pour optimiser la distribution des positions
    """

    def __init__(self, hyperliquid_client=None, lookback_periods: int = 100):
        """
        Args:
            hyperliquid_client: Client pour récupérer les données de marché
            lookback_periods: Nombre de périodes pour calculer la corrélation
        """
        self.client = hyperliquid_client
        self.lookback_periods = lookback_periods
        self.logger = logging.getLogger("BTCCorrelationManager")

        # Cache des données de prix
        self.price_cache = {}
        self.btc_prices = deque(maxlen=lookback_periods)

        # Cache des corrélations calculées
        self.correlation_cache = {}
        self.cache_timestamp = None
        self.cache_duration = 300  # 5 minutes

        # Paramètres de pondération
        self.correlation_thresholds = {
            'high': 0.8,      # Corrélation très forte
            'medium': 0.6,    # Corrélation moyenne
            'low': 0.3        # Corrélation faible
        }

        # Facteurs de pondération selon la corrélation (optimisés pour leverages plus élevés)
        self.weight_factors = {
            'very_high': 0.9,  # >0.9: Réduire très légèrement (optimisé de 0.8 à 0.9)
            'high': 0.95,      # 0.8-0.9: Réduire minimalement (optimisé de 0.9 à 0.95)
            'medium': 1.0,     # 0.6-0.8: Position normale
            'low': 1.0,        # 0.3-0.6: Position normale
            'very_low': 1.1    # <0.3: Légèrement plus agressif (optimisé de 1.0 à 1.1)
        }

        # Limites d'exposition par niveau de corrélation
        self.max_correlated_exposure = {
            'very_high': 2,    # Max 2 positions très corrélées
            'high': 3,         # Max 3 positions fortement corrélées
            'medium': 5,       # Max 5 positions moyennement corrélées
            'low': 10          # Pas de limite pour faiblement corrélées
        }

        self.lock = threading.Lock()
        self.update_thread = None
        self.running = False

    def start_background_updates(self):
        """Démarre la mise à jour périodique des corrélations"""
        if not self.running:
            self.running = True
            self.update_thread = threading.Thread(target=self._background_updater)
            self.update_thread.daemon = True
            self.update_thread.start()
            self.logger.info("Background correlation updates started")

    def stop_background_updates(self):
        """Arrête les mises à jour en arrière-plan"""
        self.running = False
        if self.update_thread:
            self.update_thread.join(timeout=5)

    def _background_updater(self):
        """Thread de mise à jour périodique des corrélations"""
        while self.running:
            try:
                self.update_all_correlations()
                time.sleep(self.cache_duration)
            except Exception as e:
                self.logger.error(f"Error in background updater: {e}")
                time.sleep(60)

    def get_price_history(self, symbol: str, periods: int = None) -> List[float]:
        """Récupère l'historique des prix d'un symbole"""
        if not self.client:
            return []

        periods = periods or self.lookback_periods

        try:
            # Utiliser get_candles_data au lieu de get_candles
            candles = self.client.get_candles_data(symbol, "1h", lookback_hours=periods)
            if candles:
                # Extraire les prix de clôture
                prices = [float(c.get('close', 0)) for c in candles if c.get('close')]
                return prices[-periods:] if len(prices) > periods else prices
        except Exception as e:
            self.logger.error(f"Error fetching price history for {symbol}: {e}")

        return []

    def calculate_correlation(self, symbol: str, btc_prices: List[float] = None) -> float:
        """
        Calcule la corrélation entre un symbole et BTC

        Returns:
            Coefficient de corrélation entre -1 et 1
        """
        try:
            # Utiliser les prix BTC fournis ou les récupérer
            if btc_prices is None:
                btc_prices = self.get_price_history("BTC")

            if not btc_prices:
                return 0.5  # Valeur neutre par défaut

            # Récupérer les prix du symbole
            symbol_prices = self.get_price_history(symbol)

            if not symbol_prices or len(symbol_prices) != len(btc_prices):
                return 0.5

            # Calculer les rendements logarithmiques
            btc_returns = np.diff(np.log(btc_prices))
            symbol_returns = np.diff(np.log(symbol_prices))

            # Calculer la corrélation
            if len(btc_returns) > 0 and len(symbol_returns) > 0:
                correlation = np.corrcoef(btc_returns, symbol_returns)[0, 1]

                # Gérer les NaN
                if np.isnan(correlation):
                    return 0.5

                return float(correlation)

        except Exception as e:
            self.logger.error(f"Error calculating correlation for {symbol}: {e}")

        return 0.5  # Valeur neutre en cas d'erreur

    def update_all_correlations(self, symbols: List[str] = None):
        """Met à jour le cache des corrélations pour tous les symboles"""
        with self.lock:
            try:
                # Récupérer les prix BTC une seule fois
                btc_prices = self.get_price_history("BTC")

                if not btc_prices:
                    self.logger.warning("Could not fetch BTC prices for correlation update")
                    return

                # Si pas de symboles fournis, utiliser ceux en cache
                if not symbols and self.client:
                    # Récupérer la liste des symboles disponibles
                    try:
                        meta = self.client.get_meta()
                        if meta and 'universe' in meta:
                            symbols = [asset['name'] for asset in meta['universe']]
                    except:
                        symbols = ['ETH', 'SOL', 'ARB', 'MATIC', 'AVAX', 'BNB',
                                 'LINK', 'UNI', 'AAVE', 'CRV', 'ALGO', 'GRT']

                # Calculer les corrélations
                new_cache = {}
                for symbol in symbols:
                    if symbol != 'BTC':
                        correlation = self.calculate_correlation(symbol, btc_prices)
                        new_cache[symbol] = {
                            'correlation': correlation,
                            'weight_factor': self.get_weight_factor(correlation),
                            'category': self.get_correlation_category(correlation)
                        }

                self.correlation_cache = new_cache
                self.cache_timestamp = datetime.now()

                self.logger.info(f"Updated correlations for {len(new_cache)} symbols")

            except Exception as e:
                self.logger.error(f"Error updating correlations: {e}")

    def get_correlation_category(self, correlation: float) -> str:
        """Catégorise le niveau de corrélation"""
        abs_corr = abs(correlation)

        if abs_corr > 0.9:
            return 'very_high'
        elif abs_corr > 0.8:
            return 'high'
        elif abs_corr > 0.6:
            return 'medium'
        elif abs_corr > 0.3:
            return 'low'
        else:
            return 'very_low'

    def get_weight_factor(self, correlation: float) -> float:
        """
        Retourne le facteur de pondération basé sur la corrélation

        Returns:
            Facteur entre 0.4 et 1.2
        """
        category = self.get_correlation_category(correlation)
        return self.weight_factors.get(category, 1.0)

    def get_position_weight(self, symbol: str, base_size: float) -> float:
        """
        Calcule la taille de position pondérée selon la corrélation

        Args:
            symbol: Le symbole à trader
            base_size: Taille de position de base

        Returns:
            Taille de position ajustée
        """
        # Vérifier le cache
        if self.needs_cache_update():
            self.update_all_correlations()

        if symbol in self.correlation_cache:
            weight_factor = self.correlation_cache[symbol]['weight_factor']
            return base_size * weight_factor

        # Calculer à la volée si pas en cache
        correlation = self.calculate_correlation(symbol)
        weight_factor = self.get_weight_factor(correlation)

        return base_size * weight_factor

    def should_open_position(self, symbol: str, current_positions: Dict) -> Tuple[bool, str]:
        """
        Détermine si une nouvelle position peut être ouverte selon les corrélations

        Args:
            symbol: Symbole à trader
            current_positions: Positions actuelles

        Returns:
            (peut_ouvrir, raison)
        """
        # Vérifier le cache
        if self.needs_cache_update():
            self.update_all_correlations()

        # Obtenir la corrélation du symbole
        if symbol not in self.correlation_cache:
            correlation = self.calculate_correlation(symbol)
            category = self.get_correlation_category(correlation)
        else:
            category = self.correlation_cache[symbol]['category']

        # Compter les positions par catégorie de corrélation
        category_counts = {'very_high': 0, 'high': 0, 'medium': 0, 'low': 0, 'very_low': 0}

        for pos_symbol in current_positions.keys():
            if pos_symbol in self.correlation_cache:
                pos_category = self.correlation_cache[pos_symbol]['category']
                category_counts[pos_category] = category_counts.get(pos_category, 0) + 1

        # Vérifier les limites d'exposition
        if category in ['very_high', 'high']:
            current_high_corr = category_counts['very_high'] + category_counts['high']
            max_allowed = self.max_correlated_exposure.get('high', 3)

            if current_high_corr >= max_allowed:
                return False, f"Limite de positions fortement corrélées atteinte ({current_high_corr}/{max_allowed})"

        # Calculer l'exposition totale pondérée
        total_weighted_exposure = sum(
            category_counts[cat] * (2.0 if cat == 'very_high' else
                                   1.5 if cat == 'high' else
                                   1.0 if cat == 'medium' else 0.5)
            for cat in category_counts
        )

        # Limite d'exposition totale pondérée
        if total_weighted_exposure > 10:
            return False, f"Exposition totale pondérée trop élevée ({total_weighted_exposure:.1f}/10)"

        return True, "OK"

    def get_portfolio_metrics(self, positions: Dict) -> Dict:
        """
        Calcule les métriques de corrélation du portefeuille

        Returns:
            Dictionnaire avec les métriques du portefeuille
        """
        if not positions:
            return {
                'avg_correlation': 0,
                'max_correlation': 0,
                'diversification_score': 1.0,
                'risk_concentration': 0
            }

        correlations = []
        for symbol in positions.keys():
            if symbol in self.correlation_cache:
                correlations.append(abs(self.correlation_cache[symbol]['correlation']))
            else:
                corr = self.calculate_correlation(symbol)
                correlations.append(abs(corr))

        if not correlations:
            return {
                'avg_correlation': 0,
                'max_correlation': 0,
                'diversification_score': 1.0,
                'risk_concentration': 0
            }

        # Calculer les métriques
        avg_corr = np.mean(correlations)
        max_corr = np.max(correlations)

        # Score de diversification (0 = pas diversifié, 1 = très diversifié)
        diversification = 1.0 - avg_corr

        # Concentration du risque (proportion de positions très corrélées)
        high_corr_count = sum(1 for c in correlations if c > 0.8)
        risk_concentration = high_corr_count / len(correlations) if correlations else 0

        return {
            'avg_correlation': float(avg_corr),
            'max_correlation': float(max_corr),
            'diversification_score': float(diversification),
            'risk_concentration': float(risk_concentration),
            'total_positions': len(positions),
            'high_corr_positions': high_corr_count
        }

    def needs_cache_update(self) -> bool:
        """Vérifie si le cache doit être mis à jour"""
        if not self.cache_timestamp:
            return True

        elapsed = (datetime.now() - self.cache_timestamp).total_seconds()
        return elapsed > self.cache_duration

    def get_recommended_position_distribution(self, available_balance: float,
                                            signals: List[Dict]) -> List[Dict]:
        """
        Recommande une distribution optimale des positions selon les corrélations

        Args:
            available_balance: Balance disponible pour trading
            signals: Liste des signaux de trading

        Returns:
            Liste des signaux avec tailles de position ajustées
        """
        if not signals:
            return []

        # Mettre à jour les corrélations si nécessaire
        if self.needs_cache_update():
            self.update_all_correlations()

        # Trier les signaux par score et corrélation
        enhanced_signals = []
        for signal in signals:
            symbol = signal.get('symbol', '')

            if symbol in self.correlation_cache:
                corr_data = self.correlation_cache[symbol]
                correlation = corr_data['correlation']
                weight_factor = corr_data['weight_factor']
            else:
                correlation = self.calculate_correlation(symbol)
                weight_factor = self.get_weight_factor(correlation)

            # Score ajusté = score original * facteur de pondération
            original_score = signal.get('score', 0.5)
            adjusted_score = original_score * weight_factor

            enhanced_signal = signal.copy()
            enhanced_signal['correlation'] = correlation
            enhanced_signal['weight_factor'] = weight_factor
            enhanced_signal['adjusted_score'] = adjusted_score
            enhanced_signal['correlation_category'] = self.get_correlation_category(correlation)

            enhanced_signals.append(enhanced_signal)

        # Trier par score ajusté décroissant
        enhanced_signals.sort(key=lambda x: x['adjusted_score'], reverse=True)

        # Allouer les positions
        allocated_signals = []
        total_allocated = 0
        category_counts = {'very_high': 0, 'high': 0, 'medium': 0, 'low': 0, 'very_low': 0}

        for signal in enhanced_signals:
            category = signal['correlation_category']

            # Vérifier les limites par catégorie
            if category in ['very_high', 'high']:
                total_high = category_counts['very_high'] + category_counts['high']
                if total_high >= self.max_correlated_exposure['high']:
                    signal['skip_reason'] = 'Limite corrélation haute atteinte'
                    continue

            # Calculer la taille de position
            base_position_size = available_balance * 0.1  # 10% par position de base
            adjusted_size = base_position_size * signal['weight_factor']

            # Vérifier qu'on ne dépasse pas le budget
            if total_allocated + adjusted_size > available_balance * 0.95:
                signal['skip_reason'] = 'Budget insuffisant'
                continue

            # Allouer la position
            signal['position_size'] = adjusted_size
            signal['position_percent'] = (adjusted_size / available_balance) * 100
            allocated_signals.append(signal)

            total_allocated += adjusted_size
            category_counts[category] = category_counts.get(category, 0) + 1

            # Limite de 10 positions max
            if len(allocated_signals) >= 10:
                break

        self.logger.info(f"Position distribution: {len(allocated_signals)} positions, "
                        f"Total allocated: ${total_allocated:.2f} ({(total_allocated/available_balance)*100:.1f}%)")

        return allocated_signals