"""
Smart Entry Criteria - Critères d'entrée simplifiés et efficaces
================================================================
Module principal pour les décisions d'achat/vente basé sur 4 règles claires:

RÈGLES D'ACHAT (Zone d'intérêt idéale):
1. BB Squeeze - Courbes Bollinger qui se resserrent (stabilité)
2. EMA9 < EMA21 - EMA9 passe en dessous de EMA21 = point d'entrée
3. Confirmation hausse - Vérifier si hausse AVANT d'acheter
4. Vente immédiate si baisse confirmée

RÈGLES D'EXCLUSION (À éviter - NO BUY):
1. EMA en baisse permanente
2. BB et EMA en baisse permanente
3. Peu de variations (volumes et prix stables)

Créé le: 23/12/2024
"""

import numpy as np
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from enum import Enum
import logging

logger = logging.getLogger("SmartEntryCriteria")


class SignalType(Enum):
    """Types de signaux possibles"""
    ACHAT = "ACHAT"           # ✅ Signal d'achat validé
    POSSIBLE = "POSSIBLE"     # 🟡 Potentiel, en surveillance
    HOLD = "HOLD"             # ⚪ Conserver la position
    VENTE = "VENTE"           # 🔴 Signal de vente
    NO_BUY = "NO_BUY"         # ❌ Ne pas acheter
    ABANDONNEE = "ABANDONNEE" # ⚫ Valeur abandonnée


class TendanceType(Enum):
    """Types de tendance"""
    HAUSSIER = "Haussier"
    BAISSIER = "Baissier"
    NEUTRE = "Neutre"


@dataclass
class SmartAnalysis:
    """Résultat de l'analyse simplifiée"""
    symbol: str
    signal: SignalType
    score: int  # 0-100
    tendance: TendanceType
    
    # Critères remplis
    bb_squeeze: bool           # Bollinger resserrées
    ema9_under_ema21: bool     # EMA9 < EMA21 (point d'entrée)
    hausse_confirmee: bool     # Hausse confirmée après le creux
    baisse_confirmee: bool     # Baisse confirmée (vente immédiate)
    
    # Critères d'exclusion
    ema_baisse_permanente: bool  # EMA en baisse permanente
    peu_variations: bool         # Peu de variations prix/volume
    
    # Indicateurs
    ema9: float
    ema21: float
    bb_bandwidth: float
    rsi: float
    
    # Message explicatif
    raison: str
    eligible: bool  # Est éligible pour achat


class SmartEntryCriteria:
    """
    Système de critères d'entrée simplifié
    Basé sur les règles visuelles définies par l'utilisateur
    """
    
    # Seuils configurables
    BB_SQUEEZE_THRESHOLD = 3.0      # % max de bandwidth pour squeeze
    EMA_SLOPE_THRESHOLD = -0.3      # Pente EMA pour baisse permanente
    MIN_VOLUME_VARIATION = 0.5      # Variation min des volumes (ratio)
    HAUSSE_CONFIRMATION = 0.3       # % de hausse pour confirmer
    BAISSE_CONFIRMATION = -0.5      # % de baisse pour confirmer vente
    
    @staticmethod
    def _ema(prices: np.ndarray, period: int) -> float:
        """Calcule l'EMA"""
        if len(prices) < period:
            return float(prices[-1]) if len(prices) > 0 else 0.0
        mult = 2 / (period + 1)
        ema = float(np.mean(prices[:period]))
        for price in prices[period:]:
            ema = (float(price) * mult) + (ema * (1 - mult))
        return ema
    
    @staticmethod
    def _bollinger(prices: np.ndarray, period: int = 20, std_dev: int = 2) -> Tuple[float, float, float]:
        """Calcule les bandes de Bollinger (upper, mid, lower)"""
        if len(prices) < period:
            val = float(prices[-1]) if len(prices) > 0 else 0.0
            return val, val, val
        sma = float(np.mean(prices[-period:]))
        std = float(np.std(prices[-period:]))
        return sma + std_dev * std, sma, sma - std_dev * std
    
    @staticmethod
    def _rsi(prices: np.ndarray, period: int = 14) -> float:
        """Calcule le RSI"""
        if len(prices) < period + 1:
            return 50.0
        deltas = np.diff(prices)
        gains = np.where(deltas > 0, deltas, 0)
        losses = np.where(deltas < 0, -deltas, 0)
        avg_gain = float(np.mean(gains[-period:]))
        avg_loss = float(np.mean(losses[-period:]))
        if avg_loss == 0:
            return 100.0
        rs = avg_gain / avg_loss
        return 100.0 - (100.0 / (1.0 + rs))
    
    @staticmethod
    def _calculate_ema_slope(prices: np.ndarray, period: int, lookback: int = 5) -> float:
        """Calcule la pente de l'EMA sur les dernières bougies"""
        if len(prices) < period + lookback:
            return 0.0
        ema_now = SmartEntryCriteria._ema(prices, period)
        ema_prev = SmartEntryCriteria._ema(prices[:-lookback], period)
        if ema_prev == 0:
            return 0.0
        return ((ema_now - ema_prev) / ema_prev) * 100
    
    @staticmethod
    def _is_volume_active(volumes: Optional[List[float]], threshold: float = 0.5) -> bool:
        """Vérifie si les volumes sont suffisamment actifs (variation)"""
        if not volumes or len(volumes) < 10:
            return True  # Par défaut, on considère actif si pas de données
        
        vol_array = np.array(volumes[-20:])
        avg_vol = np.mean(vol_array)
        if avg_vol == 0:
            return False
        
        # Ratio entre max et min des volumes récents
        vol_range = (np.max(vol_array) - np.min(vol_array)) / avg_vol
        return vol_range > threshold
    
    @classmethod
    def analyze(cls, 
                symbol: str, 
                prices: List[float], 
                volumes: Optional[List[float]] = None) -> SmartAnalysis:
        """
        Analyse une valeur selon les critères simplifiés
        
        Args:
            symbol: Symbole de la crypto
            prices: Liste des prix (au moins 50 valeurs)
            volumes: Liste des volumes (optionnel)
            
        Returns:
            SmartAnalysis avec le résultat complet
        """
        
        # Valeurs par défaut
        default_result = SmartAnalysis(
            symbol=symbol,
            signal=SignalType.HOLD,
            score=0,
            tendance=TendanceType.NEUTRE,
            bb_squeeze=False,
            ema9_under_ema21=False,
            hausse_confirmee=False,
            baisse_confirmee=False,
            ema_baisse_permanente=False,
            peu_variations=False,
            ema9=0.0,
            ema21=0.0,
            bb_bandwidth=0.0,
            rsi=50.0,
            raison="Données insuffisantes",
            eligible=False
        )
        
        if len(prices) < 50:
            return default_result
        
        prices_arr = np.array(prices, dtype=float)
        
        # ══════════════════════════════════════════════════════════════════
        # CALCUL DES INDICATEURS
        # ══════════════════════════════════════════════════════════════════
        
        ema9 = cls._ema(prices_arr, 9)
        ema21 = cls._ema(prices_arr, 21)
        bb_upper, bb_mid, bb_lower = cls._bollinger(prices_arr)
        rsi = cls._rsi(prices_arr)
        
        # Bandwidth des Bollinger (en %)
        bb_bandwidth = ((bb_upper - bb_lower) / bb_mid * 100) if bb_mid > 0 else 0
        
        # Pentes des EMAs
        ema9_slope = cls._calculate_ema_slope(prices_arr, 9)
        ema21_slope = cls._calculate_ema_slope(prices_arr, 21)
        
        # ══════════════════════════════════════════════════════════════════
        # CRITÈRE 1: BB SQUEEZE (Bandes resserrées = stabilité)
        # ══════════════════════════════════════════════════════════════════
        bb_squeeze = bb_bandwidth < cls.BB_SQUEEZE_THRESHOLD
        
        # ══════════════════════════════════════════════════════════════════
        # CRITÈRE 2: EMA9 < EMA21 (Point d'entrée idéal - le creux)
        # ══════════════════════════════════════════════════════════════════
        ema9_under_ema21 = ema9 < ema21
        
        # ══════════════════════════════════════════════════════════════════
        # CRITÈRE 3: HAUSSE CONFIRMÉE (On achète uniquement si ça remonte)
        # ══════════════════════════════════════════════════════════════════
        # On vérifie si le prix monte sur les 3 dernières bougies
        price_change_3 = ((prices[-1] - prices[-3]) / prices[-3]) * 100 if len(prices) >= 3 else 0
        price_change_1 = ((prices[-1] - prices[-2]) / prices[-2]) * 100 if len(prices) >= 2 else 0
        
        hausse_confirmee = (
            price_change_1 > 0 and  # Dernière bougie positive
            price_change_3 > cls.HAUSSE_CONFIRMATION  # Hausse sur 3 bougies
        )
        
        # ══════════════════════════════════════════════════════════════════
        # CRITÈRE 4: BAISSE CONFIRMÉE (Vente immédiate)
        # ══════════════════════════════════════════════════════════════════
        baisse_confirmee = (
            price_change_1 < cls.BAISSE_CONFIRMATION and
            ema9_slope < 0  # EMA9 en baisse
        )
        
        # ══════════════════════════════════════════════════════════════════
        # EXCLUSION 1: EMA EN BAISSE PERMANENTE (tendance baissière)
        # ══════════════════════════════════════════════════════════════════
        ema_baisse_permanente = (
            ema9_slope < cls.EMA_SLOPE_THRESHOLD and 
            ema21_slope < cls.EMA_SLOPE_THRESHOLD and
            price_change_3 < -0.5  # Baisse sur 3 bougies
        )
        
        # ══════════════════════════════════════════════════════════════════
        # EXCLUSION 2: PEU DE VARIATIONS (pas intéressant)
        # ══════════════════════════════════════════════════════════════════
        peu_variations = not cls._is_volume_active(volumes, cls.MIN_VOLUME_VARIATION)
        
        # Vérifier aussi la variation des prix
        # Seuil réduit à 0.15% pour permettre les grandes cryptos comme BTC
        if len(prices) >= 20:
            price_volatility = float(np.std(prices[-20:]) / np.mean(prices[-20:]) * 100)
            if price_volatility < 0.15:  # Moins de 0.15% de volatilité (très bas)
                peu_variations = True
        
        # ══════════════════════════════════════════════════════════════════
        # DÉTERMINATION DE LA TENDANCE
        # ══════════════════════════════════════════════════════════════════
        
        # CROISEMENT IMMINENT: EMA9 < EMA21 mais EMA9 remonte vers EMA21
        # C'est une opportunité d'achat, pas un blocage!
        ema_diff_pct = ((ema9 - ema21) / ema21) * 100 if ema21 != 0 else 0
        is_crossover_imminent = (
            ema9 < ema21 and              # EMA9 encore sous EMA21
            ema_diff_pct > -0.8 and       # Mais proche (moins de 0.8%)
            ema9_slope > 0                # ET EMA9 remonte
        )
        
        # TENDANCE BAISSIÈRE GLOBALE: EMA9 < EMA21 ET tout descend SANS croisement
        tendance_bearish_globale = (
            ema9 < ema21 and  # EMA9 sous EMA21
            not is_crossover_imminent and  # PAS si croisement imminent!
            (ema9_slope < 0 or ema21_slope < 0) and  # Au moins une EMA descend
            price_change_3 < 0  # Prix en baisse sur 3 bougies
        )
        
        if is_crossover_imminent:
            tendance = TendanceType.HAUSSIER  # Croisement = tendance haussière!
        elif ema9 > ema21 and ema9_slope > 0 and not tendance_bearish_globale:
            tendance = TendanceType.HAUSSIER
        elif ema9 < ema21 and (ema9_slope < 0 or ema_baisse_permanente):
            tendance = TendanceType.BAISSIER
        else:
            tendance = TendanceType.NEUTRE
        
        # ══════════════════════════════════════════════════════════════════
        # CALCUL DU SCORE (0-100)
        # ══════════════════════════════════════════════════════════════════
        score = 50  # Base neutre
        raisons = []
        
        # Points positifs
        if bb_squeeze:
            score += 15
            raisons.append("BB Squeeze ✓")
        
        if ema9_under_ema21:
            score += 20
            raisons.append("EMA9 < EMA21 ✓")
        else:
            # PÉNALITÉ MAJEURE si EMA9 >= EMA21 (pas de point d'entrée)
            score -= 50
            raisons.append("EMA9 >= EMA21 ✗ (pas d'entrée)")
        
        if hausse_confirmee:
            score += 25
            raisons.append("Hausse confirmée ✓")
        
        # Points négatifs
        if baisse_confirmee:
            score -= 30
            raisons.append("Baisse confirmée ✗")
        
        if ema_baisse_permanente:
            score -= 40
            raisons.append("EMA baisse permanente ✗")
        
        if peu_variations:
            score -= 15
            raisons.append("Peu de variations ✗")
        
        # RSI bonus/malus
        if 30 <= rsi <= 40:  # Zone de survente = opportunité
            score += 10
            raisons.append(f"RSI favorable ({rsi:.0f})")
        elif rsi > 70:  # Surachat = danger
            score -= 10
            raisons.append(f"RSI surachat ({rsi:.0f})")
        
        score = max(0, min(100, score))  # Clamp 0-100
        
        # ══════════════════════════════════════════════════════════════════
        # DÉCISION FINALE
        # ══════════════════════════════════════════════════════════════════
        
        eligible = True
        
        # ══════════════════════════════════════════════════════════════════
        # PRIORITÉ #1: CROISEMENT IMMINENT = ACHAT FORT!
        # EMA9 remonte vers EMA21 = opportunité d'achat (avant le croisement)
        # ══════════════════════════════════════════════════════════════════
        if is_crossover_imminent:
            signal = SignalType.ACHAT
            score = min(100, score + 20)  # Bonus score!
            raisons.insert(0, "🔥 CROISEMENT IMMINENT - Achat!")
            eligible = True
        
        # ══════════════════════════════════════════════════════════════════
        # BLOCAGE #2: TENDANCE BAISSIÈRE SANS CROISEMENT = PAS D'ACHAT
        # ══════════════════════════════════════════════════════════════════
        elif tendance_bearish_globale:
            signal = SignalType.NO_BUY
            raisons.insert(0, "📉 TENDANCE BAISSIÈRE - Pas d'achat!")
            eligible = False
        
        # RÈGLE PRINCIPALE: EMA9 >= EMA21 = AUCUN ACHAT POSSIBLE
        # C'est la règle la plus importante - si EMA9 est au-dessus de EMA21,
        # nous ne sommes PAS au point d'entrée optimal (le creux)
        # EXCEPTION: Si EMA9 est très proche de EMA21 (< 1%) ET conditions parfaites = continuation haussière prudente
        elif not ema9_under_ema21:
            # Calculer la différence EMA en %
            ema_diff_pct = ((ema9 / ema21) - 1) * 100
            
            # CAS SPÉCIAL: Continuation haussière PRUDENTE
            # Autoriser si EMA9 est TRÈS PROCHE de EMA21 (< 1%) avec conditions strictes
            if (ema_diff_pct < 1.0 and  # Max 1% au-dessus
                bb_squeeze and           # Stabilité confirmée
                hausse_confirmee and     # Hausse en cours
                rsi < 65 and            # Pas suracheté
                not ema_baisse_permanente):  # Pas en baisse
                signal = SignalType.ACHAT
                raisons.insert(0, f"✅ Continuation haussière prudente (EMA diff={ema_diff_pct:.2f}%)")
                eligible = True
            else:
                signal = SignalType.ABANDONNEE
                raisons.insert(0, "❌ EMA9 >= EMA21 (pas de point d'entrée)")
                eligible = False
        
        # EXCLUSIONS (NO BUY absolu)
        elif ema_baisse_permanente:
            signal = SignalType.NO_BUY
            raisons.insert(0, "❌ EMA en baisse permanente")
            eligible = False
        
        elif peu_variations and not hausse_confirmee:
            # Peu de variations ET pas de hausse = vraiment mort
            signal = SignalType.NO_BUY
            raisons.insert(0, "❌ Peu de variations")
            eligible = False
        
        elif baisse_confirmee:
            signal = SignalType.VENTE
            raisons.insert(0, "🔴 Vente immédiate")
            eligible = False
        
        # CONDITIONS D'ACHAT CLASSIQUE (creux)
        elif bb_squeeze and ema9_under_ema21 and hausse_confirmee:
            # ✅ TOUTES les conditions remplies = ACHAT
            signal = SignalType.ACHAT
            raisons.insert(0, "✅ Signal d'ACHAT")
            eligible = True
        
        elif bb_squeeze and ema9_under_ema21:
            # 🟡 2 conditions sur 3 = POSSIBLE (en surveillance)
            signal = SignalType.POSSIBLE
            raisons.insert(0, "🟡 En surveillance (attente hausse)")
            eligible = True
        
        elif ema9_under_ema21 and hausse_confirmee:
            # 🟡 Point d'entrée + hausse mais pas squeeze = POSSIBLE
            signal = SignalType.POSSIBLE
            raisons.insert(0, "🟡 Possible (pas de squeeze)")
            eligible = True
        
        else:
            signal = SignalType.HOLD
            raisons.insert(0, "⚪ En attente")
            eligible = False
        
        return SmartAnalysis(
            symbol=symbol,
            signal=signal,
            score=score,
            tendance=tendance,
            bb_squeeze=bb_squeeze,
            ema9_under_ema21=ema9_under_ema21,
            hausse_confirmee=hausse_confirmee,
            baisse_confirmee=baisse_confirmee,
            ema_baisse_permanente=ema_baisse_permanente,
            peu_variations=peu_variations,
            ema9=ema9,
            ema21=ema21,
            bb_bandwidth=bb_bandwidth,
            rsi=rsi,
            raison=" | ".join(raisons),
            eligible=eligible
        )
    
    @classmethod
    def filter_eligible(cls, analyses: List[SmartAnalysis]) -> List[SmartAnalysis]:
        """
        Filtre les analyses pour ne garder que les valeurs éligibles
        Tri par score décroissant
        """
        eligible = [a for a in analyses if a.eligible]
        return sorted(eligible, key=lambda x: x.score, reverse=True)
    
    @classmethod
    def get_top_opportunities(cls, analyses: List[SmartAnalysis], limit: int = 10) -> List[SmartAnalysis]:
        """
        Retourne les meilleures opportunités d'achat
        Tri par score et signal
        """
        # Priorité: ACHAT > POSSIBLE > autres
        def priority_key(a: SmartAnalysis):
            signal_priority = {
                SignalType.ACHAT: 0,
                SignalType.POSSIBLE: 1,
                SignalType.HOLD: 2,
                SignalType.VENTE: 3,
                SignalType.NO_BUY: 4,
                SignalType.ABANDONNEE: 5
            }
            return (signal_priority.get(a.signal, 5), -a.score)
        
        sorted_analyses = sorted(analyses, key=priority_key)
        return sorted_analyses[:limit]


# ══════════════════════════════════════════════════════════════════════════════
# FONCTION HELPER POUR INTÉGRATION RAPIDE
# ══════════════════════════════════════════════════════════════════════════════

def quick_analyze(symbol: str, prices: List[float], volumes: List[float] = None) -> Dict:
    """
    Analyse rapide retournant un dictionnaire simple
    Pour intégration facile dans le trading bot
    """
    result = SmartEntryCriteria.analyze(symbol, prices, volumes)
    
    return {
        'symbol': result.symbol,
        'signal': result.signal.value,
        'score': result.score,
        'tendance': result.tendance.value,
        'eligible': result.eligible,
        'raison': result.raison,
        'criteria': {
            'bb_squeeze': result.bb_squeeze,
            'ema9_under_ema21': result.ema9_under_ema21,
            'hausse_confirmee': result.hausse_confirmee,
            'baisse_confirmee': result.baisse_confirmee
        },
        'exclusions': {
            'ema_baisse_permanente': result.ema_baisse_permanente,
            'peu_variations': result.peu_variations
        },
        'indicators': {
            'ema9': result.ema9,
            'ema21': result.ema21,
            'bb_bandwidth': result.bb_bandwidth,
            'rsi': result.rsi
        }
    }


# Instance globale pour accès facile
_smart_criteria_instance = None

def get_smart_criteria() -> SmartEntryCriteria:
    """Retourne l'instance globale des critères"""
    global _smart_criteria_instance
    if _smart_criteria_instance is None:
        _smart_criteria_instance = SmartEntryCriteria()
    return _smart_criteria_instance


if __name__ == "__main__":
    # Test rapide
    import random
    
    # Générer des prix de test
    test_prices = [100.0]
    for _ in range(99):
        change = random.uniform(-0.02, 0.025)  # Légère tendance haussière
        test_prices.append(test_prices[-1] * (1 + change))
    
    result = SmartEntryCriteria.analyze("TEST/USDT", test_prices)
    
    print(f"\n{'='*60}")
    print(f"ANALYSE: {result.symbol}")
    print(f"{'='*60}")
    print(f"Signal: {result.signal.value}")
    print(f"Score: {result.score}/100")
    print(f"Tendance: {result.tendance.value}")
    print(f"Éligible: {'Oui' if result.eligible else 'Non'}")
    print(f"\nCritères:")
    print(f"  - BB Squeeze: {'✓' if result.bb_squeeze else '✗'}")
    print(f"  - EMA9 < EMA21: {'✓' if result.ema9_under_ema21 else '✗'}")
    print(f"  - Hausse confirmée: {'✓' if result.hausse_confirmee else '✗'}")
    print(f"\nExclusions:")
    print(f"  - EMA baisse permanente: {'✗' if result.ema_baisse_permanente else '✓'}")
    print(f"  - Peu de variations: {'✗' if result.peu_variations else '✓'}")
    print(f"\nRaison: {result.raison}")
