from __future__ import annotations

from typing import Dict, Tuple, List
import math

try:  # pragma: no cover - runtime dependency may be missing/broken
    import pandas as pd  # type: ignore
    import numpy as np  # type: ignore
    _PANDAS_OK = True
except Exception:  # pragma: no cover - runtime dependency may be missing/broken
    pd = None  # type: ignore
    np = None  # type: ignore
    _PANDAS_OK = False

class IndicatorCalculator:
    @staticmethod
    def calculate_vwap(df: pd.DataFrame) -> pd.Series:
        if not _PANDAS_OK:
            return None  # type: ignore[return-value]
        if 'volume' not in df or df['volume'].sum() == 0:
            return pd.Series(np.nan, index=df.index)
        typical_price = (df['high'] + df['low'] + df['close']) / 3
        cumulative_tp_volume = (typical_price * df['volume']).cumsum()
        cumulative_volume = df['volume'].cumsum()
        return cumulative_tp_volume / cumulative_volume

    @staticmethod
    def calculate_ema(df: pd.DataFrame, period: int) -> pd.Series:
        if not _PANDAS_OK:
            return None  # type: ignore[return-value]
        return df['close'].ewm(span=period, adjust=False).mean()

    @staticmethod
    def calculate_rsi(df: pd.DataFrame, period: int = 14) -> pd.Series:
        if not _PANDAS_OK:
            return None  # type: ignore[return-value]
        delta = df['close'].diff()
        gain = delta.where(delta > 0, 0)
        loss = -delta.where(delta < 0, 0)
        
        # Use Wilder's smoothing
        avg_gain = gain.ewm(alpha=1/period, adjust=False).mean()
        avg_loss = loss.ewm(alpha=1/period, adjust=False).mean()
        
        rs = avg_gain / avg_loss
        rsi = 100 - (100 / (1 + rs))
        return rsi

    @staticmethod
    def calculate_macd(
        df: pd.DataFrame, 
        fast: int = 12, 
        slow: int = 26, 
        signal: int = 9
    ) -> Tuple[pd.Series, pd.Series, pd.Series]:
        if not _PANDAS_OK:
            return None, None, None  # type: ignore[return-value]
        ema_fast = df['close'].ewm(span=fast, adjust=False).mean()
        ema_slow = df['close'].ewm(span=slow, adjust=False).mean()
        macd_line = ema_fast - ema_slow
        signal_line = macd_line.ewm(span=signal, adjust=False).mean()
        histogram = macd_line - signal_line
        return macd_line, signal_line, histogram

    @staticmethod
    def calculate_pivot_points(df: pd.DataFrame) -> Dict[str, float]:
        """Calculate Standard Pivot Points (R3, R2, R1, P, S1, S2, S3)"""
        if not _PANDAS_OK:
            return {}
        if len(df) < 1:
            return {}
        
        prev = df.iloc[-1]
        p = (prev['high'] + prev['low'] + prev['close']) / 3
        r1 = (2 * p) - prev['low']
        s1 = (2 * p) - prev['high']
        r2 = p + (prev['high'] - prev['low'])
        s2 = p - (prev['high'] - prev['low'])
        r3 = p + 2 * (prev['high'] - prev['low'])
        s3 = p - 2 * (prev['high'] - prev['low'])
        return {'R1': r1, 'R2': r2, 'R3': r3, 'P': p, 'S1': s1, 'S2': s2, 'S3': s3}

    @staticmethod
    def calculate_ema_stack(df: pd.DataFrame, periods: list) -> Dict[str, pd.Series]:
        if not _PANDAS_OK:
            return {}
        emas = {}
        for period in periods:
            emas[f'ema_{period}'] = IndicatorCalculator.calculate_ema(df, period)
        return emas

    @staticmethod
    def calculate_volume_delta(df: pd.DataFrame) -> pd.Series:
        if not _PANDAS_OK:
            return None  # type: ignore[return-value]
        return df['volume'].diff()
    
    @staticmethod
    def add_all_indicators(df: pd.DataFrame, ema_periods: list = None) -> pd.DataFrame:
        """Add all technical indicators to DataFrame"""
        if not _PANDAS_OK:
            return df
        if ema_periods is None:
            ema_periods = [5, 9, 15, 21, 30, 55, 100, 200]
        
        # VWAP
        df['vwap'] = IndicatorCalculator.calculate_vwap(df)
        
        # RSI
        df['rsi'] = IndicatorCalculator.calculate_rsi(df)
        
        # MACD
        df['macd'], df['macd_signal'], df['macd_hist'] = IndicatorCalculator.calculate_macd(df)
        
        # EMA Stack
        ema_stack = IndicatorCalculator.calculate_ema_stack(df, ema_periods)
        for name, series in ema_stack.items():
            df[name] = series
        
        # Volume Delta
        df['volume_delta'] = IndicatorCalculator.calculate_volume_delta(df)
        
        return df
    
    @staticmethod
    def calculate_supertrend(df: pd.DataFrame, period: int = 10, multiplier: float = 3.0) -> pd.Series:
        """
        Calculate Supertrend indicator
        Returns: SuperTrend values and direction (1 for uptrend, -1 for downtrend)
        """
        if not _PANDAS_OK:
            return None, None  # type: ignore[return-value]
        hl2 = (df['high'] + df['low']) / 2
        atr = df['high'].combine(df['low'], lambda x, y: x - y).abs().rolling(period).mean()
        
        upper_band = hl2 + multiplier * atr
        lower_band = hl2 - multiplier * atr
        
        supertrend = pd.Series(index=df.index)
        direction = pd.Series(1, index=df.index)  # 1 = uptrend, -1 = downtrend
        
        for i in range(1, len(df)):
            if df['close'].iloc[i-1] <= supertrend.iloc[i-1]:
                # Previous trend was uptrend
                supertrend.iloc[i] = min(lower_band.iloc[i], supertrend.iloc[i-1])
                if df['close'].iloc[i] < supertrend.iloc[i]:
                    direction.iloc[i] = -1
            else:
                # Previous trend was downtrend
                supertrend.iloc[i] = max(upper_band.iloc[i], supertrend.iloc[i-1])
                if df['close'].iloc[i] > supertrend.iloc[i]:
                    direction.iloc[i] = 1
                    
        return supertrend, direction
    
    @staticmethod
    def calculate_multi_timeframe_pivots(timeframe_data: Dict[str, pd.DataFrame]) -> Dict[str, Dict[str, float]]:
        if not _PANDAS_OK:
            return {}
        pivots = {}
        for tf, df in timeframe_data.items():
            if df is None or df.empty:
                continue

            # Normalise timeframe labels (accept both "30min" and "30minute" styles)
            tf_norm = tf.lower().replace(" ", "")
            if tf_norm in ["30min", "30minute", "day", "week", "month"]:
                pivots[tf] = IndicatorCalculator.calculate_pivot_points(df)
        return pivots

    @staticmethod
    def calculate_fibonacci_levels(df: pd.DataFrame, lookback: int = 100) -> Dict[str, Dict[str, float]]:
        """Calculate simple Fibonacci retracement and extension levels.

        Uses the highest high and lowest low over the last `lookback`
        bars (or all available if fewer) to define a swing, then
        computes standard retracement and extension levels.
        """

        if not _PANDAS_OK or df is None or df.empty:
            return {}

        window = df.tail(lookback)
        if window.empty:
            return {}

        swing_high = float(window["high"].max())
        swing_low = float(window["low"].min())

        if swing_high == swing_low:
            return {}

        diff = swing_high - swing_low

        # Basic retracement levels (0-100%)
        retracements = {
            "0": swing_low,
            "23.6": swing_high - 0.236 * diff,
            "38.2": swing_high - 0.382 * diff,
            "50": swing_high - 0.5 * diff,
            "61.8": swing_high - 0.618 * diff,
            "78.6": swing_high - 0.786 * diff,
            "100": swing_high,
        }

        # Simple upward extensions from swing high
        extensions = {
            "127.2": swing_high + 0.272 * diff,
            "161.8": swing_high + 0.618 * diff,
        }

        return {
            "swing_high": swing_high,
            "swing_low": swing_low,
            "retracements": retracements,
            "extensions": extensions,
        }

    @staticmethod
    def summarize_dataframe(df: pd.DataFrame, ema_periods: List[int] = None) -> Dict[str, any]:
        """Return a compact summary of key indicators for the latest bar.

        Expects a DataFrame with at least columns: high, low, close and
        ideally volume. This is intended to be used by higher-level
        services (like teGPT) to build structured numeric features for
        GPT prompts or internal scoring, without bloating the payload
        with full indicator series.
        """

        if not _PANDAS_OK or df is None or df.empty:
            return {}

        if ema_periods is None:
            ema_periods = [5, 9, 15, 21, 30, 55, 100, 200]

        # Work on a copy to avoid mutating the original frame upstream.
        df_ind = IndicatorCalculator.add_all_indicators(df.copy(), ema_periods=ema_periods)

        last = df_ind.iloc[-1]

        def _finite_float(v):
            try:
                if v is None or isinstance(v, bool):
                    return None
                f = float(v)
                return f if math.isfinite(f) else None
            except Exception:
                return None

        ema_values: Dict[str, float] = {}
        for period in ema_periods:
            col = f"ema_{period}"
            if col in df_ind.columns:
                ema_values[str(period)] = _finite_float(last.get(col))

        summary: Dict[str, any] = {
            "close": _finite_float(last.get("close")),
            "vwap": _finite_float(last.get("vwap")),
            "rsi": _finite_float(last.get("rsi")),
            "macd": _finite_float(last.get("macd")),
            "macd_signal": _finite_float(last.get("macd_signal")),
            "macd_hist": _finite_float(last.get("macd_hist")),
            "volume": _finite_float(last.get("volume")) if "volume" in df_ind.columns else None,
            "volume_delta": _finite_float(last.get("volume_delta")),
            "ema": ema_values,
        }

        return summary