"""Comprehensive technical indicator library using pandas-ta and TA-Lib."""

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

# Try to import pandas_ta, but handle if numba is missing
try:
    import pandas_ta as ta
    PANDAS_TA_AVAILABLE = True
except ImportError:
    PANDAS_TA_AVAILABLE = False
    ta = None
    import warnings
    warnings.warn("pandas-ta not available (numba issue), using basic implementations")

try:
    import talib
    TALIB_AVAILABLE = True
except ImportError:
    TALIB_AVAILABLE = False

from src.core.logger import get_logger

logger = get_logger(__name__)


class TechnicalIndicators:
    """Technical indicators library."""
    
    def __init__(self):
        """Initialize indicators library."""
        self.talib_available = TALIB_AVAILABLE
    
    # Trend Indicators
    
    def sma(self, data: pd.Series, period: int = 20) -> pd.Series:
        """Simple Moving Average."""
        if PANDAS_TA_AVAILABLE:
            return ta.sma(data, length=period)
        return data.rolling(window=period).mean()
    
    def ema(self, data: pd.Series, period: int = 20) -> pd.Series:
        """Exponential Moving Average."""
        if PANDAS_TA_AVAILABLE:
            return ta.ema(data, length=period)
        return data.ewm(span=period, adjust=False).mean()
    
    def wma(self, data: pd.Series, period: int = 20) -> pd.Series:
        """Weighted Moving Average."""
        if PANDAS_TA_AVAILABLE:
            return ta.wma(data, length=period)
        # Basic WMA implementation
        weights = np.arange(1, period + 1)
        return data.rolling(window=period).apply(lambda x: np.dot(x, weights) / weights.sum(), raw=True)
    
    def dema(self, data: pd.Series, period: int = 20) -> pd.Series:
        """Double Exponential Moving Average."""
        if PANDAS_TA_AVAILABLE:
            return ta.dema(data, length=period)
        ema1 = self.ema(data, period)
        return 2 * ema1 - self.ema(ema1, period)
    
    def tema(self, data: pd.Series, period: int = 20) -> pd.Series:
        """Triple Exponential Moving Average."""
        if PANDAS_TA_AVAILABLE:
            return ta.tema(data, length=period)
        ema1 = self.ema(data, period)
        ema2 = self.ema(ema1, period)
        ema3 = self.ema(ema2, period)
        return 3 * ema1 - 3 * ema2 + ema3
    
    # Momentum Indicators
    
    def rsi(self, data: pd.Series, period: int = 14) -> pd.Series:
        """Relative Strength Index."""
        if self.talib_available:
            return pd.Series(talib.RSI(data.values, timeperiod=period), index=data.index)
        if PANDAS_TA_AVAILABLE:
            return ta.rsi(data, length=period)
        # Basic RSI implementation
        delta = data.diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
        rs = gain / loss
        return 100 - (100 / (1 + rs))
    
    def macd(
        self,
        data: pd.Series,
        fast: int = 12,
        slow: int = 26,
        signal: int = 9
    ) -> Dict[str, pd.Series]:
        """MACD (Moving Average Convergence Divergence)."""
        if self.talib_available:
            macd, signal_line, histogram = talib.MACD(
                data.values, fastperiod=fast, slowperiod=slow, signalperiod=signal
            )
            return {
                'macd': pd.Series(macd, index=data.index),
                'signal': pd.Series(signal_line, index=data.index),
                'histogram': pd.Series(histogram, index=data.index),
            }
        if not PANDAS_TA_AVAILABLE or ta is None:
            # Basic MACD implementation fallback
            ema_fast = self.ema(data, fast)
            ema_slow = self.ema(data, slow)
            macd_line = ema_fast - ema_slow
            signal_line = self.ema(macd_line.dropna(), signal)
            histogram = macd_line - signal_line
            return {
                'macd': macd_line,
                'signal': signal_line,
                'histogram': histogram,
            }
        result = ta.macd(data, fast=fast, slow=slow, signal=signal)
        return {
            'macd': result[f'MACD_{fast}_{slow}_{signal}'],
            'signal': result[f'MACDs_{fast}_{slow}_{signal}'],
            'histogram': result[f'MACDh_{fast}_{slow}_{signal}'],
        }
    
    def stochastic(
        self,
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        k_period: int = 14,
        d_period: int = 3
    ) -> Dict[str, pd.Series]:
        """Stochastic Oscillator."""
        if self.talib_available:
            slowk, slowd = talib.STOCH(
                high.values, low.values, close.values,
                fastk_period=k_period, slowk_period=d_period, slowd_period=d_period
            )
            return {
                'k': pd.Series(slowk, index=close.index),
                'd': pd.Series(slowd, index=close.index),
            }
        if PANDAS_TA_AVAILABLE and ta is not None:
            result = ta.stoch(high, low, close, k=k_period, d=d_period)
            return {
                'k': result[f'STOCHk_{k_period}_{d_period}_{d_period}'],
                'd': result[f'STOCHd_{k_period}_{d_period}_{d_period}'],
            }
        # Basic Stochastic implementation
        lowest_low = low.rolling(window=k_period).min()
        highest_high = high.rolling(window=k_period).max()
        k = 100 * ((close - lowest_low) / (highest_high - lowest_low))
        d = k.rolling(window=d_period).mean()
        return {'k': k, 'd': d}
    
    def cci(
        self,
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        period: int = 20
    ) -> pd.Series:
        """Commodity Channel Index."""
        if self.talib_available:
            return pd.Series(
                talib.CCI(high.values, low.values, close.values, timeperiod=period),
                index=close.index
            )
        if PANDAS_TA_AVAILABLE:
            return ta.cci(high, low, close, length=period)
        # Basic CCI implementation
        tp = (high + low + close) / 3
        sma_tp = tp.rolling(window=period).mean()
        mad = tp.rolling(window=period).apply(lambda x: np.abs(x - x.mean()).mean(), raw=True)
        return (tp - sma_tp) / (0.015 * mad)
    
    def williams_r(
        self,
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        period: int = 14
    ) -> pd.Series:
        """Williams %R."""
        if self.talib_available:
            return pd.Series(
                talib.WILLR(high.values, low.values, close.values, timeperiod=period),
                index=close.index
            )
        if PANDAS_TA_AVAILABLE:
            return ta.willr(high, low, close, length=period)
        # Basic Williams %R implementation
        highest_high = high.rolling(window=period).max()
        lowest_low = low.rolling(window=period).min()
        return -100 * ((highest_high - close) / (highest_high - lowest_low))
    
    # Volatility Indicators
    
    def bollinger_bands(
        self,
        data: pd.Series,
        period: int = 20,
        std_dev: float = 2.0
    ) -> Dict[str, pd.Series]:
        """Bollinger Bands."""
        if self.talib_available:
            upper, middle, lower = talib.BBANDS(
                data.values, timeperiod=period, nbdevup=std_dev, nbdevdn=std_dev
            )
            return {
                'upper': pd.Series(upper, index=data.index),
                'middle': pd.Series(middle, index=data.index),
                'lower': pd.Series(lower, index=data.index),
            }
        if PANDAS_TA_AVAILABLE:
            result = ta.bbands(data, length=period, std=std_dev)
            return {
                'upper': result[f'BBU_{period}_{std_dev}'],
                'middle': result[f'BBM_{period}_{std_dev}'],
                'lower': result[f'BBL_{period}_{std_dev}'],
            }
        # Basic Bollinger Bands implementation
        middle = self.sma(data, period)
        std = data.rolling(window=period).std()
        return {
            'upper': middle + (std * std_dev),
            'middle': middle,
            'lower': middle - (std * std_dev),
        }
    
    def atr(
        self,
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        period: int = 14
    ) -> pd.Series:
        """Average True Range."""
        if self.talib_available:
            return pd.Series(
                talib.ATR(high.values, low.values, close.values, timeperiod=period),
                index=close.index
            )
        if PANDAS_TA_AVAILABLE and ta is not None:
            return ta.atr(high, low, close, length=period)
        # Basic ATR implementation
        prev_close = close.shift(1)
        tr1 = high - low
        tr2 = abs(high - prev_close)
        tr3 = abs(low - prev_close)
        tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
        return tr.rolling(window=period).mean()
    
    def keltner_channels(
        self,
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        period: int = 20,
        multiplier: float = 2.0
    ) -> Dict[str, pd.Series]:
        """Keltner Channels."""
        if PANDAS_TA_AVAILABLE and ta is not None:
            return ta.kc(high, low, close, length=period, scalar=multiplier)
        # Basic Keltner Channels implementation
        middle = self.ema(close, period)
        atr_val = self.atr(high, low, close, period)
        return {
            'lower': middle - (multiplier * atr_val),
            'middle': middle,
            'upper': middle + (multiplier * atr_val),
        }
    
    # Volume Indicators
    
    def obv(self, close: pd.Series, volume: pd.Series) -> pd.Series:
        """On-Balance Volume."""
        if self.talib_available:
            return pd.Series(
                talib.OBV(close.values, volume.values),
                index=close.index
            )
        if PANDAS_TA_AVAILABLE and ta is not None:
            return ta.obv(close, volume)
        # Basic OBV implementation
        price_change = close.diff()
        obv = pd.Series(index=close.index, dtype=float)
        obv.iloc[0] = 0
        for i in range(1, len(close)):
            if price_change.iloc[i] > 0:
                obv.iloc[i] = obv.iloc[i-1] + volume.iloc[i]
            elif price_change.iloc[i] < 0:
                obv.iloc[i] = obv.iloc[i-1] - volume.iloc[i]
            else:
                obv.iloc[i] = obv.iloc[i-1]
        return obv
    
    def vwap(
        self,
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        volume: pd.Series
    ) -> pd.Series:
        """Volume Weighted Average Price."""
        if PANDAS_TA_AVAILABLE and ta is not None:
            return ta.vwap(high, low, close, volume)
        # Basic VWAP implementation
        typical_price = (high + low + close) / 3
        cumulative_tp_vol = (typical_price * volume).cumsum()
        cumulative_vol = volume.cumsum()
        return cumulative_tp_vol / cumulative_vol
    
    # Advanced Indicators
    
    def ichimoku(
        self,
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        tenkan: int = 9,
        kijun: int = 26,
        senkou: int = 52
    ) -> Dict[str, pd.Series]:
        """Ichimoku Cloud."""
        if PANDAS_TA_AVAILABLE and ta is not None:
            result = ta.ichimoku(high, low, close, tenkan=tenkan, kijun=kijun, senkou=senkou)
            return {
                'tenkan': result['ITS_9'],
                'kijun': result['IKS_26'],
                'senkou_a': result['ISA_9'],
                'senkou_b': result['ISB_26'],
                'chikou': result['ICS_26'],
            }
        # Basic Ichimoku implementation
        tenkan_sen = (high.rolling(window=tenkan).max() + low.rolling(window=tenkan).min()) / 2
        kijun_sen = (high.rolling(window=kijun).max() + low.rolling(window=kijun).min()) / 2
        senkou_a = ((tenkan_sen + kijun_sen) / 2).shift(kijun)
        senkou_b = ((high.rolling(window=senkou).max() + low.rolling(window=senkou).min()) / 2).shift(kijun)
        chikou = close.shift(-kijun)
        return {
            'tenkan': tenkan_sen,
            'kijun': kijun_sen,
            'senkou_a': senkou_a,
            'senkou_b': senkou_b,
            'chikou': chikou,
        }
    
    def adx(
        self,
        high: pd.Series,
        low: pd.Series,
        close: pd.Series,
        period: int = 14
    ) -> pd.Series:
        """Average Directional Index."""
        if self.talib_available:
            return pd.Series(
                talib.ADX(high.values, low.values, close.values, timeperiod=period),
                index=close.index
            )
        if PANDAS_TA_AVAILABLE and ta is not None:
            return ta.adx(high, low, close, length=period)
        # Basic ADX implementation
        plus_dm = high.diff()
        minus_dm = low.diff().abs()
        plus_dm[plus_dm < 0] = 0
        minus_dm[minus_dm < 0] = 0
        
        atr_val = self.atr(high, low, close, period)
        plus_di = 100 * (plus_dm.rolling(window=period).mean() / atr_val)
        minus_di = 100 * (minus_dm.rolling(window=period).mean() / atr_val)
        
        dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di)
        adx = dx.rolling(window=period).mean()
        return adx
    
    def detect_divergence(
        self,
        prices: pd.Series,
        indicator: pd.Series,
        lookback: int = 20,
        min_swings: int = 2
    ) -> Dict[str, Any]:
        """Detect divergence between price and indicator.
        
        Divergence occurs when price makes new highs/lows but indicator doesn't,
        or vice versa. This is a powerful reversal signal.
        
        Args:
            prices: Price series
            indicator: Indicator series (e.g., RSI, MACD)
            lookback: Lookback period for finding swings
            min_swings: Minimum number of swings to detect divergence
            
        Returns:
            Dictionary with divergence information:
            {
                'type': 'bullish', 'bearish', or None
                'confidence': 0.0 to 1.0
                'price_swing_high': price at high swing
                'price_swing_low': price at low swing
                'indicator_swing_high': indicator at high swing
                'indicator_swing_low': indicator at low swing
            }
        """
        if len(prices) < lookback * 2 or len(indicator) < lookback * 2:
            return {
                'type': None,
                'confidence': 0.0,
                'price_swing_high': None,
                'price_swing_low': None,
                'indicator_swing_high': None,
                'indicator_swing_low': None
            }
        
        # Find local extrema (swings)
        def find_swings(series: pd.Series, lookback: int):
            """Find local maxima and minima."""
            highs = []
            lows = []
            
            for i in range(lookback, len(series) - lookback):
                window = series.iloc[i-lookback:i+lookback+1]
                center = series.iloc[i]
                
                # Local maximum
                if center == window.max():
                    highs.append((i, center))
                # Local minimum
                elif center == window.min():
                    lows.append((i, center))
            
            return highs, lows
        
        price_highs, price_lows = find_swings(prices, lookback)
        indicator_highs, indicator_lows = find_swings(indicator, lookback)
        
        # Need at least min_swings swings to detect divergence
        if len(price_highs) < min_swings or len(price_lows) < min_swings:
            return {
                'type': None,
                'confidence': 0.0,
                'price_swing_high': None,
                'price_swing_low': None,
                'indicator_swing_high': None,
                'indicator_swing_low': None
            }
        
        # Check for bearish divergence (price makes higher high, indicator makes lower high)
        if len(price_highs) >= 2 and len(indicator_highs) >= 2:
            recent_price_high = price_highs[-1][1]
            prev_price_high = price_highs[-2][1]
            recent_indicator_high = indicator_highs[-1][1]
            prev_indicator_high = indicator_highs[-2][1]
            
            # Price higher high but indicator lower high = bearish divergence
            if recent_price_high > prev_price_high and recent_indicator_high < prev_indicator_high:
                confidence = min(1.0, abs(recent_price_high - prev_price_high) / prev_price_high * 10)
                return {
                    'type': 'bearish',
                    'confidence': confidence,
                    'price_swing_high': (price_highs[-2][0], price_highs[-1][0]),
                    'price_swing_low': None,
                    'indicator_swing_high': (indicator_highs[-2][0], indicator_highs[-1][0]),
                    'indicator_swing_low': None
                }
        
        # Check for bullish divergence (price makes lower low, indicator makes higher low)
        if len(price_lows) >= 2 and len(indicator_lows) >= 2:
            recent_price_low = price_lows[-1][1]
            prev_price_low = price_lows[-2][1]
            recent_indicator_low = indicator_lows[-1][1]
            prev_indicator_low = indicator_lows[-2][1]
            
            # Price lower low but indicator higher low = bullish divergence
            if recent_price_low < prev_price_low and recent_indicator_low > prev_indicator_low:
                confidence = min(1.0, abs(prev_price_low - recent_price_low) / prev_price_low * 10)
                return {
                    'type': 'bullish',
                    'confidence': confidence,
                    'price_swing_high': None,
                    'price_swing_low': (price_lows[-2][0], price_lows[-1][0]),
                    'indicator_swing_high': None,
                    'indicator_swing_low': (indicator_lows[-2][0], indicator_lows[-1][0])
                }
        
        return {
            'type': None,
            'confidence': 0.0,
            'price_swing_high': None,
            'price_swing_low': None,
            'indicator_swing_high': None,
            'indicator_swing_low': None
        }
    
    def calculate_all(
        self,
        df: pd.DataFrame,
        indicators: Optional[List[str]] = None
    ) -> pd.DataFrame:
        """Calculate multiple indicators at once.
        
        Args:
            df: DataFrame with OHLCV data (columns: open, high, low, close, volume)
            indicators: List of indicator names to calculate (None = all)
            
        Returns:
            DataFrame with added indicator columns
        """
        result = df.copy()
        
        if 'close' not in result.columns:
            raise ValueError("DataFrame must have 'close' column")
        
        close = result['close']
        high = result.get('high', close)
        low = result.get('low', close)
        volume = result.get('volume', pd.Series(1, index=close.index))
        
        # Default indicators if none specified
        if indicators is None:
            indicators = [
                'sma_20', 'ema_20', 'rsi', 'macd', 'bollinger_bands',
                'atr', 'obv', 'adx'
            ]
        
        for indicator in indicators:
            try:
                if indicator.startswith('sma_'):
                    period = int(indicator.split('_')[1])
                    result[f'SMA_{period}'] = self.sma(close, period)
                elif indicator.startswith('ema_'):
                    period = int(indicator.split('_')[1])
                    result[f'EMA_{period}'] = self.ema(close, period)
                elif indicator == 'rsi':
                    result['RSI'] = self.rsi(close)
                elif indicator == 'macd':
                    macd_data = self.macd(close)
                    result['MACD'] = macd_data['macd']
                    result['MACD_Signal'] = macd_data['signal']
                    result['MACD_Histogram'] = macd_data['histogram']
                elif indicator == 'bollinger_bands':
                    bb_data = self.bollinger_bands(close)
                    result['BB_Upper'] = bb_data['upper']
                    result['BB_Middle'] = bb_data['middle']
                    result['BB_Lower'] = bb_data['lower']
                elif indicator == 'atr':
                    result['ATR'] = self.atr(high, low, close)
                elif indicator == 'obv':
                    result['OBV'] = self.obv(close, volume)
                elif indicator == 'adx':
                    result['ADX'] = self.adx(high, low, close)
            except Exception as e:
                logger.warning(f"Failed to calculate indicator {indicator}: {e}")
        
        return result


# Global indicators instance
_indicators: Optional[TechnicalIndicators] = None


def get_indicators() -> TechnicalIndicators:
    """Get global technical indicators instance."""
    global _indicators
    if _indicators is None:
        _indicators = TechnicalIndicators()
    return _indicators

