import json
import random
from typing import Any, Optional, Dict
import numpy as np
import pandas as pd
import talib
import datetime as dt

# A dictionary to hold both indicator types and their corresponding classes.
indicators_registry = {}


# Function to generate random hex color
def generate_random_color():
    return f"#{random.randrange(0x1000000):06x}"


class Indicator:
    def __init__(self, name: str, indicator_type: str, properties: dict):
        self.name = name
        self.properties = properties
        self.properties.setdefault('type', indicator_type)
        self.properties.setdefault('value', 0)
        self.properties.setdefault('period', 14)

    def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> dict:
        """
        Default calculation for indicators that follows a standard process:
        - Converts the 'close' prices to NumPy array
        - Uses the 'process' method to apply the indicator calculation
        - Returns the result as a DataFrame
        """
        # Converts the close prices from the DataFrame to a NumPy array of floats
        closes = candles.close.to_numpy(dtype='float')

        # Processing the close prices to calculate the Indicator
        i_values = self.process(closes, int(self.properties['period']))

        # Stores the last calculated value.
        self.properties['value'] = round(float(i_values[-1]), 2)

        # Create a DataFrame with 'time' and 'value'
        df = pd.DataFrame({'time': candles.time, 'value': i_values.tolist()})

        # Slice the DataFrame to skip initial rows where the indicator will be undefined
        return df.iloc[int(self.properties['period']):]

    def process(self, data, period):
        """
        Abstract method that should be overridden in the subclass.
        """
        raise NotImplementedError("Subclasses must implement the 'process' method.")


class Volume(Indicator):
    def __init__(self, name: str, indicator_type: str, properties: dict):
        super().__init__(name, indicator_type, properties)
        # Default display properties for Volume
        self.properties.setdefault('color_up', 'rgba(0, 150, 136, 0.8)')  # Green for increased volume
        self.properties.setdefault('color_down', 'rgba(255, 82, 82, 0.8)')  # Red for decreased volume

    def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame:
        """
        Custom calculation for Volume that doesn't need NaN handling.
        Returns a DataFrame with 'time', 'value', and 'color' columns.
        """

        def get_color(row):
            row_index = row.name
            if (row_index - 1) not in volumes.index:
                return self.properties['color_up']  # Default green
            if cndls.close.iloc[row_index - 1] < cndls.close.iloc[row_index]:
                return self.properties['color_up']  # Increased volume (green)
            else:
                return self.properties['color_down']  # Decreased volume (red)

        cndls = candles.copy().reset_index(drop=True)

        # Extract the open time and volume columns
        volumes = cndls.loc[:, ['time', 'volume']]
        volumes['color'] = volumes.apply(get_color, axis=1)

        # Rename the volume column to 'value'
        volumes = volumes.rename(columns={'volume': 'value'})
        current_volume = volumes['value'].iloc[-1]
        self.properties['value'] = float(current_volume)

        # Return the DataFrame for consistency
        return volumes


class SMA(Indicator):
    def __init__(self, name: str, indicator_type: str, properties: dict):
        super().__init__(name, indicator_type, properties)
        # Default display properties for SMA
        self.properties.setdefault('color', generate_random_color())
        self.properties.setdefault('thickness', 1)  # Default line thickness
        self.properties.setdefault('period', 20)

    def process(self, data: np.ndarray, period: int) -> np.ndarray:
        """
        Calculate the Simple Moving Average (SMA) of the given data.
        """
        return talib.SMA(data, int(period))


class EMA(SMA):
    def process(self, data: np.ndarray, period: int) -> np.ndarray:
        """
        Calculate the Exponential Moving Average (EMA) of the given data.
        """
        return talib.EMA(data, int(period))


class RSI(SMA):
    def __init__(self, name: str, indicator_type: str, properties: dict):
        super().__init__(name, indicator_type, properties)
        # Default display properties for RSI
        self.properties.setdefault('period', 14)

    def process(self, data: np.ndarray, period: int) -> np.ndarray:
        """
        Calculate the Relative Strength Index (RSI) of the given data.
        """
        return talib.RSI(data, int(period))


class LREG(SMA):
    def process(self, data: np.ndarray, period: int) -> np.ndarray:
        """
        Calculate the Linear Regression (LREG) of the given data.
        """
        return talib.LINEARREG(data, int(period))


class ATR(SMA):
    def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame:
        """
        Calculate the Average True Range (ATR) indicator.
        """
        highs = candles.high.to_numpy(dtype='float')
        lows = candles.low.to_numpy(dtype='float')
        closes = candles.close.to_numpy(dtype='float')

        # Calculate ATR using the talib library
        atr = talib.ATR(high=highs, low=lows, close=closes, timeperiod=int(self.properties['period']))

        # Create DataFrame with 'time' and 'value' columns
        df = pd.DataFrame({'time': candles.time, 'value': atr})

        # Store the last calculated ATR value
        self.properties['value'] = round(float(atr[-1]), 2)

        # Return the sliced DataFrame, excluding rows where the indicator is not fully calculated
        return df.iloc[int(self.properties['period']):]


class BolBands(Indicator):
    def __init__(self, name: str, indicator_type: str, properties: dict):
        super().__init__(name, indicator_type, properties)
        # Default display properties
        self.properties.setdefault('period', 20)  # The default period for the moving average
        self.properties.setdefault('devup', 2)  # Standard deviation for the upper band
        self.properties.setdefault('devdn', 2)  # Standard deviation for the lower band
        self.properties.setdefault('ma', 0)  # Moving average type (0 = Simple Moving Average in TA-Lib)
        self.properties.setdefault('color_1', generate_random_color())  # Upper band
        self.properties.setdefault('color_2', generate_random_color())  # Middle band
        self.properties.setdefault('color_3', generate_random_color())  # Lower band

    def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame:
        """
        Calculate the Bollinger Bands indicator for the given candles.
        """
        np_real_data = candles.close.to_numpy(dtype='float')

        # Calculate the Bollinger Bands (upper, middle, lower)
        upper, middle, lower = talib.BBANDS(np_real_data,
                                            timeperiod=int(self.properties['period']),
                                            nbdevup=int(self.properties['devup']),
                                            nbdevdn=int(self.properties['devdn']),
                                            matype=int(self.properties['ma']))

        # Store the last calculated values in properties
        self.properties['value'] = round(float(upper[-1]), 2)
        self.properties['value2'] = round(float(middle[-1]), 2)
        self.properties['value3'] = round(float(lower[-1]), 2)

        # Create a DataFrame with 'time', 'upper', 'middle', 'lower'
        df = pd.DataFrame({
            'time': candles.time,
            'upper': upper,
            'middle': middle,
            'lower': lower
        })

        # Round all values at once
        df = df.round({'upper': 2, 'middle': 2, 'lower': 2})

        # Slice the DataFrame to skip initial rows where the indicator might be undefined
        return df.iloc[int(self.properties['period']):]


class BollingerPercentB(Indicator):
    """
    Bollinger %B indicator shows where price is relative to the bands.
    %B = (Close - Lower Band) / (Upper Band - Lower Band)

    Values:
    - %B = 1.0: Price at upper band
    - %B = 0.5: Price at middle band
    - %B = 0.0: Price at lower band
    - %B > 1.0: Price above upper band (overbought)
    - %B < 0.0: Price below lower band (oversold)
    """
    def __init__(self, name: str, indicator_type: str, properties: dict):
        super().__init__(name, indicator_type, properties)
        # Default display properties (same as Bollinger Bands)
        self.properties.setdefault('period', 20)
        self.properties.setdefault('devup', 2)
        self.properties.setdefault('devdn', 2)
        self.properties.setdefault('ma', 0)  # Moving average type (0 = SMA)
        self.properties.setdefault('color', generate_random_color())
        self.properties.setdefault('thickness', 1)

    def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame:
        """
        Calculate Bollinger %B indicator.
        """
        closes = candles.close.to_numpy(dtype='float')

        # Calculate Bollinger Bands
        upper, middle, lower = talib.BBANDS(
            closes,
            timeperiod=int(self.properties['period']),
            nbdevup=int(self.properties['devup']),
            nbdevdn=int(self.properties['devdn']),
            matype=int(self.properties['ma'])
        )

        # Calculate %B: (Close - Lower) / (Upper - Lower)
        # Handle division by zero when bands are equal
        band_width = upper - lower
        percent_b = np.where(band_width != 0, (closes - lower) / band_width, 0.5)

        # Store the last calculated value
        self.properties['value'] = round(float(percent_b[-1]), 4)

        # Create DataFrame with 'time' and 'value'
        df = pd.DataFrame({
            'time': candles.time,
            'value': percent_b
        })

        # Round values
        df = df.round({'value': 4})

        # Slice to skip initial rows where indicator is undefined
        return df.iloc[int(self.properties['period']):]


class MACD(Indicator):
    def __init__(self, name: str, indicator_type: str, properties: dict):
        super().__init__(name, indicator_type, properties)
        # Default display properties
        self.properties.setdefault('fast_p', 12)
        self.properties.setdefault('slow_p', 26)
        self.properties.setdefault('signal_p', 9)

        self.properties.setdefault('color_1', generate_random_color())  # Upper band
        self.properties.setdefault('color_2', generate_random_color())  # Middle band
        self.properties.setdefault('color_3', generate_random_color())  # Lower band

    def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame:
        """
        Calculate the MACD indicator for the given candles.
        """
        closes = candles.close.to_numpy(dtype='float')

        # Calculate MACD, Signal Line, and MACD Histogram
        macd, signal, hist = talib.MACD(closes,
                                        fastperiod=int(self.properties['fast_p']),
                                        slowperiod=int(self.properties['slow_p']),
                                        signalperiod=int(self.properties['signal_p']))

        # Store the last calculated values
        self.properties['macd'] = round(float(macd[-1]), 2)
        self.properties['signal'] = round(float(signal[-1]), 2)
        self.properties['hist'] = round(float(hist[-1]), 2)

        # Create a DataFrame with 'time', 'macd', 'signal', 'hist'
        df = pd.DataFrame({
            'time': candles.time,
            'macd': macd,
            'signal': signal,
            'hist': hist
        })

        # Replace NaN values with None for JSON compatibility
        df = df.replace({np.nan: None})

        # Slice the DataFrame to skip initial rows where the indicator will be undefined
        return df.iloc[self.properties['signal_p']:]


# ============================================================================
# Candlestick Pattern Recognition Indicators
# ============================================================================

class CandlestickPattern(Indicator):
    """
    Base class for candlestick pattern recognition indicators.

    Pattern indicators return discrete signals:
    - 100 = Bullish pattern detected
    - -100 = Bearish pattern detected
    - 0 = No pattern

    All patterns require OHLC data (open, high, low, close).
    """

    def __init__(self, name: str, indicator_type: str, properties: dict):
        super().__init__(name, indicator_type, properties)
        # Default display properties for candlestick patterns (hex format for color picker compatibility)
        self.properties.setdefault('bullish_color', '#00C853')
        self.properties.setdefault('bearish_color', '#FF5252')
        # Patterns don't use period in the same way - set to 0 to avoid warmup trimming
        self.properties.setdefault('period', 0)

    def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame:
        """
        Calculate pattern detection across all candles.
        Returns a DataFrame with 'time' and 'value' columns.
        """
        # Extract OHLC data as numpy arrays
        opens = candles.open.to_numpy(dtype='float')
        highs = candles.high.to_numpy(dtype='float')
        lows = candles.low.to_numpy(dtype='float')
        closes = candles.close.to_numpy(dtype='float')

        # Call the pattern-specific detection method
        pattern_values = self.detect(opens, highs, lows, closes)

        # Store the latest value
        self.properties['value'] = int(pattern_values[-1])

        # Create DataFrame with signal values
        df = pd.DataFrame({
            'time': candles.time,
            'value': pattern_values.astype(int).tolist()
        })

        return df

    def detect(self, opens: np.ndarray, highs: np.ndarray,
               lows: np.ndarray, closes: np.ndarray) -> np.ndarray:
        """
        Override in subclass to call specific TA-Lib pattern function.
        Must return a numpy array of pattern signals (100, -100, or 0).
        """
        raise NotImplementedError("Subclasses must implement the 'detect' method.")


# Single Candle Patterns (6)

class CDL_Doji(CandlestickPattern):
    """Doji - Indecision pattern where open and close are nearly equal."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLDOJI(opens, highs, lows, closes)


class CDL_Hammer(CandlestickPattern):
    """Hammer - Bullish reversal pattern with small body and long lower shadow."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLHAMMER(opens, highs, lows, closes)


class CDL_InvertedHammer(CandlestickPattern):
    """Inverted Hammer - Bullish reversal pattern with small body and long upper shadow."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLINVERTEDHAMMER(opens, highs, lows, closes)


class CDL_ShootingStar(CandlestickPattern):
    """Shooting Star - Bearish reversal pattern with small body and long upper shadow."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLSHOOTINGSTAR(opens, highs, lows, closes)


class CDL_SpinningTop(CandlestickPattern):
    """Spinning Top - Indecision pattern with small body and equal shadows."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLSPINNINGTOP(opens, highs, lows, closes)


class CDL_Marubozu(CandlestickPattern):
    """Marubozu - Strong trend candle with no shadows (or very small)."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLMARUBOZU(opens, highs, lows, closes)


# Two Candle Patterns (4)

class CDL_Engulfing(CandlestickPattern):
    """Engulfing - Reversal pattern where second candle completely engulfs the first."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLENGULFING(opens, highs, lows, closes)


class CDL_Harami(CandlestickPattern):
    """Harami - Reversal pattern where second candle is contained within the first."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLHARAMI(opens, highs, lows, closes)


class CDL_Piercing(CandlestickPattern):
    """Piercing Line - Bullish reversal where second candle opens below and closes above midpoint."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLPIERCING(opens, highs, lows, closes)


class CDL_DarkCloudCover(CandlestickPattern):
    """Dark Cloud Cover - Bearish reversal where second candle opens above and closes below midpoint."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLDARKCLOUDCOVER(opens, highs, lows, closes)


# Three+ Candle Patterns (5)

class CDL_MorningStar(CandlestickPattern):
    """Morning Star - Bullish reversal with three candles: bearish, small body, bullish."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLMORNINGSTAR(opens, highs, lows, closes)


class CDL_EveningStar(CandlestickPattern):
    """Evening Star - Bearish reversal with three candles: bullish, small body, bearish."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLEVENINGSTAR(opens, highs, lows, closes)


class CDL_ThreeWhiteSoldiers(CandlestickPattern):
    """Three White Soldiers - Strong bullish pattern with three consecutive bullish candles."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDL3WHITESOLDIERS(opens, highs, lows, closes)


class CDL_ThreeBlackCrows(CandlestickPattern):
    """Three Black Crows - Strong bearish pattern with three consecutive bearish candles."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDL3BLACKCROWS(opens, highs, lows, closes)


class CDL_ThreeInside(CandlestickPattern):
    """Three Inside Up/Down - Reversal pattern confirming a harami with a third candle."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDL3INSIDE(opens, highs, lows, closes)


class CDL_TwoCrows(CandlestickPattern):
    """Two Crows - Bearish reversal pattern with gap up followed by bearish engulfing."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDL2CROWS(opens, highs, lows, closes)


class CDL_ThreeLineStrike(CandlestickPattern):
    """Three Line Strike - Continuation pattern with three candles followed by opposite engulfing."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDL3LINESTRIKE(opens, highs, lows, closes)


class CDL_ThreeOutside(CandlestickPattern):
    """Three Outside Up/Down - Engulfing pattern confirmed by third candle."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDL3OUTSIDE(opens, highs, lows, closes)


class CDL_ThreeStarsInSouth(CandlestickPattern):
    """Three Stars in South - Rare bullish reversal with three decreasing bearish candles."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDL3STARSINSOUTH(opens, highs, lows, closes)


class CDL_AbandonedBaby(CandlestickPattern):
    """Abandoned Baby - Strong reversal with doji gap isolated from surrounding candles."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLABANDONEDBABY(opens, highs, lows, closes)


class CDL_AdvanceBlock(CandlestickPattern):
    """Advance Block - Bearish reversal showing weakening bullish momentum."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLADVANCEBLOCK(opens, highs, lows, closes)


class CDL_BeltHold(CandlestickPattern):
    """Belt Hold - Single candle reversal opening at extreme and closing near opposite."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLBELTHOLD(opens, highs, lows, closes)


class CDL_Breakaway(CandlestickPattern):
    """Breakaway - Five candle reversal pattern breaking away from trend."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLBREAKAWAY(opens, highs, lows, closes)


class CDL_ClosingMarubozu(CandlestickPattern):
    """Closing Marubozu - Marubozu with no shadow on closing end."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLCLOSINGMARUBOZU(opens, highs, lows, closes)


class CDL_ConcealBabySwallow(CandlestickPattern):
    """Concealing Baby Swallow - Rare bullish pattern with four candles."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLCONCEALBABYSWALL(opens, highs, lows, closes)


class CDL_CounterAttack(CandlestickPattern):
    """Counter Attack - Two candle reversal with opposite colors closing at same price."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLCOUNTERATTACK(opens, highs, lows, closes)


class CDL_DojiStar(CandlestickPattern):
    """Doji Star - Doji that gaps away from previous candle, signaling reversal."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLDOJISTAR(opens, highs, lows, closes)


class CDL_DragonflyDoji(CandlestickPattern):
    """Dragonfly Doji - Doji with long lower shadow, potential bullish reversal."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLDRAGONFLYDOJI(opens, highs, lows, closes)


class CDL_EveningDojiStar(CandlestickPattern):
    """Evening Doji Star - Bearish reversal with doji as the star."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLEVENINGDOJISTAR(opens, highs, lows, closes)


class CDL_GapSideSideWhite(CandlestickPattern):
    """Gap Side-by-Side White - Continuation pattern with gap and two similar candles."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLGAPSIDESIDEWHITE(opens, highs, lows, closes)


class CDL_GravestoneDoji(CandlestickPattern):
    """Gravestone Doji - Doji with long upper shadow, potential bearish reversal."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLGRAVESTONEDOJI(opens, highs, lows, closes)


class CDL_HangingMan(CandlestickPattern):
    """Hanging Man - Hammer-like pattern at top of uptrend, bearish signal."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLHANGINGMAN(opens, highs, lows, closes)


class CDL_HaramiCross(CandlestickPattern):
    """Harami Cross - Harami pattern where second candle is a doji."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLHARAMICROSS(opens, highs, lows, closes)


class CDL_HighWave(CandlestickPattern):
    """High Wave - Candle with very long upper and lower shadows, extreme indecision."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLHIGHWAVE(opens, highs, lows, closes)


class CDL_Hikkake(CandlestickPattern):
    """Hikkake - Inside bar breakout failure pattern."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLHIKKAKE(opens, highs, lows, closes)


class CDL_HikkakeMod(CandlestickPattern):
    """Modified Hikkake - Variation of hikkake with specific confirmation."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLHIKKAKEMOD(opens, highs, lows, closes)


class CDL_HomingPigeon(CandlestickPattern):
    """Homing Pigeon - Bullish harami variant with both candles bearish."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLHOMINGPIGEON(opens, highs, lows, closes)


class CDL_IdenticalThreeCrows(CandlestickPattern):
    """Identical Three Crows - Three crows opening at previous close."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLIDENTICAL3CROWS(opens, highs, lows, closes)


class CDL_InNeck(CandlestickPattern):
    """In-Neck - Bearish continuation with second candle closing near first's low."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLINNECK(opens, highs, lows, closes)


class CDL_Kicking(CandlestickPattern):
    """Kicking - Two marubozu candles of opposite color with gap between."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLKICKING(opens, highs, lows, closes)


class CDL_KickingByLength(CandlestickPattern):
    """Kicking By Length - Kicking pattern determined by longer marubozu."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLKICKINGBYLENGTH(opens, highs, lows, closes)


class CDL_LadderBottom(CandlestickPattern):
    """Ladder Bottom - Five candle bullish reversal pattern."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLLADDERBOTTOM(opens, highs, lows, closes)


class CDL_LongLeggedDoji(CandlestickPattern):
    """Long-Legged Doji - Doji with very long upper and lower shadows."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLLONGLEGGEDDOJI(opens, highs, lows, closes)


class CDL_LongLine(CandlestickPattern):
    """Long Line Candle - Candle with unusually long body."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLLONGLINE(opens, highs, lows, closes)


class CDL_MatchingLow(CandlestickPattern):
    """Matching Low - Two candles with same low, potential bullish reversal."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLMATCHINGLOW(opens, highs, lows, closes)


class CDL_MatHold(CandlestickPattern):
    """Mat Hold - Five candle bullish continuation pattern."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLMATHOLD(opens, highs, lows, closes)


class CDL_MorningDojiStar(CandlestickPattern):
    """Morning Doji Star - Bullish reversal with doji as the star."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLMORNINGDOJISTAR(opens, highs, lows, closes)


class CDL_OnNeck(CandlestickPattern):
    """On-Neck - Bearish continuation with second candle closing at first's low."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLONNECK(opens, highs, lows, closes)


class CDL_RickshawMan(CandlestickPattern):
    """Rickshaw Man - Long-legged doji with open/close at center."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLRICKSHAWMAN(opens, highs, lows, closes)


class CDL_RiseFallThreeMethods(CandlestickPattern):
    """Rising/Falling Three Methods - Five candle continuation pattern."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLRISEFALL3METHODS(opens, highs, lows, closes)


class CDL_SeparatingLines(CandlestickPattern):
    """Separating Lines - Continuation pattern with opposite candle opening at same price."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLSEPARATINGLINES(opens, highs, lows, closes)


class CDL_ShortLine(CandlestickPattern):
    """Short Line Candle - Candle with unusually short body."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLSHORTLINE(opens, highs, lows, closes)


class CDL_StalledPattern(CandlestickPattern):
    """Stalled Pattern - Bearish reversal showing weakening uptrend."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLSTALLEDPATTERN(opens, highs, lows, closes)


class CDL_StickSandwich(CandlestickPattern):
    """Stick Sandwich - Bullish reversal with matching lows around opposite candle."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLSTICKSANDWICH(opens, highs, lows, closes)


class CDL_Takuri(CandlestickPattern):
    """Takuri - Dragonfly doji variant with very long lower shadow."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLTAKURI(opens, highs, lows, closes)


class CDL_TasukiGap(CandlestickPattern):
    """Tasuki Gap - Continuation pattern with gap partially filled."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLTASUKIGAP(opens, highs, lows, closes)


class CDL_Thrusting(CandlestickPattern):
    """Thrusting - Bearish continuation similar to in-neck but closing higher."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLTHRUSTING(opens, highs, lows, closes)


class CDL_TriStar(CandlestickPattern):
    """Tri-Star - Three doji pattern signaling major reversal."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLTRISTAR(opens, highs, lows, closes)


class CDL_UniqueThreeRiver(CandlestickPattern):
    """Unique Three River - Rare bullish reversal with three specific candles."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLUNIQUE3RIVER(opens, highs, lows, closes)


class CDL_UpsideGapTwoCrows(CandlestickPattern):
    """Upside Gap Two Crows - Bearish reversal with gap up followed by two bearish candles."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLUPSIDEGAP2CROWS(opens, highs, lows, closes)


class CDL_XSideGapThreeMethods(CandlestickPattern):
    """Side Gap Three Methods - Continuation pattern filling a gap."""
    def detect(self, opens, highs, lows, closes):
        return talib.CDLXSIDEGAP3METHODS(opens, highs, lows, closes)


# Register indicators in the registry
indicators_registry['Volume'] = Volume
indicators_registry['SMA'] = SMA
indicators_registry['EMA'] = EMA
indicators_registry['RSI'] = RSI
indicators_registry['LREG'] = LREG
indicators_registry['ATR'] = ATR
indicators_registry['BOLBands'] = BolBands
indicators_registry['BOL%B'] = BollingerPercentB
indicators_registry['MACD'] = MACD

# Register candlestick pattern indicators
# Single Candle Patterns
indicators_registry['CDL_DOJI'] = CDL_Doji
indicators_registry['CDL_HAMMER'] = CDL_Hammer
indicators_registry['CDL_INVERTEDHAMMER'] = CDL_InvertedHammer
indicators_registry['CDL_SHOOTINGSTAR'] = CDL_ShootingStar
indicators_registry['CDL_SPINNINGTOP'] = CDL_SpinningTop
indicators_registry['CDL_MARUBOZU'] = CDL_Marubozu
# Two Candle Patterns
indicators_registry['CDL_ENGULFING'] = CDL_Engulfing
indicators_registry['CDL_HARAMI'] = CDL_Harami
indicators_registry['CDL_PIERCING'] = CDL_Piercing
indicators_registry['CDL_DARKCLOUDCOVER'] = CDL_DarkCloudCover
# Three+ Candle Patterns
indicators_registry['CDL_MORNINGSTAR'] = CDL_MorningStar
indicators_registry['CDL_EVENINGSTAR'] = CDL_EveningStar
indicators_registry['CDL_3WHITESOLDIERS'] = CDL_ThreeWhiteSoldiers
indicators_registry['CDL_3BLACKCROWS'] = CDL_ThreeBlackCrows
indicators_registry['CDL_3INSIDE'] = CDL_ThreeInside
indicators_registry['CDL_2CROWS'] = CDL_TwoCrows
indicators_registry['CDL_3LINESTRIKE'] = CDL_ThreeLineStrike
indicators_registry['CDL_3OUTSIDE'] = CDL_ThreeOutside
indicators_registry['CDL_3STARSINSOUTH'] = CDL_ThreeStarsInSouth
indicators_registry['CDL_ABANDONEDBABY'] = CDL_AbandonedBaby
indicators_registry['CDL_ADVANCEBLOCK'] = CDL_AdvanceBlock
indicators_registry['CDL_BELTHOLD'] = CDL_BeltHold
indicators_registry['CDL_BREAKAWAY'] = CDL_Breakaway
indicators_registry['CDL_CLOSINGMARUBOZU'] = CDL_ClosingMarubozu
indicators_registry['CDL_CONCEALBABYSWALL'] = CDL_ConcealBabySwallow
indicators_registry['CDL_COUNTERATTACK'] = CDL_CounterAttack
indicators_registry['CDL_DOJISTAR'] = CDL_DojiStar
indicators_registry['CDL_DRAGONFLYDOJI'] = CDL_DragonflyDoji
indicators_registry['CDL_EVENINGDOJISTAR'] = CDL_EveningDojiStar
indicators_registry['CDL_GAPSIDESIDEWHITE'] = CDL_GapSideSideWhite
indicators_registry['CDL_GRAVESTONEDOJI'] = CDL_GravestoneDoji
indicators_registry['CDL_HANGINGMAN'] = CDL_HangingMan
indicators_registry['CDL_HARAMICROSS'] = CDL_HaramiCross
indicators_registry['CDL_HIGHWAVE'] = CDL_HighWave
indicators_registry['CDL_HIKKAKE'] = CDL_Hikkake
indicators_registry['CDL_HIKKAKEMOD'] = CDL_HikkakeMod
indicators_registry['CDL_HOMINGPIGEON'] = CDL_HomingPigeon
indicators_registry['CDL_IDENTICAL3CROWS'] = CDL_IdenticalThreeCrows
indicators_registry['CDL_INNECK'] = CDL_InNeck
indicators_registry['CDL_KICKING'] = CDL_Kicking
indicators_registry['CDL_KICKINGBYLENGTH'] = CDL_KickingByLength
indicators_registry['CDL_LADDERBOTTOM'] = CDL_LadderBottom
indicators_registry['CDL_LONGLEGGEDDOJI'] = CDL_LongLeggedDoji
indicators_registry['CDL_LONGLINE'] = CDL_LongLine
indicators_registry['CDL_MATCHINGLOW'] = CDL_MatchingLow
indicators_registry['CDL_MATHOLD'] = CDL_MatHold
indicators_registry['CDL_MORNINGDOJISTAR'] = CDL_MorningDojiStar
indicators_registry['CDL_ONNECK'] = CDL_OnNeck
indicators_registry['CDL_RICKSHAWMAN'] = CDL_RickshawMan
indicators_registry['CDL_RISEFALL3METHODS'] = CDL_RiseFallThreeMethods
indicators_registry['CDL_SEPARATINGLINES'] = CDL_SeparatingLines
indicators_registry['CDL_SHORTLINE'] = CDL_ShortLine
indicators_registry['CDL_STALLEDPATTERN'] = CDL_StalledPattern
indicators_registry['CDL_STICKSANDWICH'] = CDL_StickSandwich
indicators_registry['CDL_TAKURI'] = CDL_Takuri
indicators_registry['CDL_TASUKIGAP'] = CDL_TasukiGap
indicators_registry['CDL_THRUSTING'] = CDL_Thrusting
indicators_registry['CDL_TRISTAR'] = CDL_TriStar
indicators_registry['CDL_UNIQUE3RIVER'] = CDL_UniqueThreeRiver
indicators_registry['CDL_UPSIDEGAP2CROWS'] = CDL_UpsideGapTwoCrows
indicators_registry['CDL_XSIDEGAP3METHODS'] = CDL_XSideGapThreeMethods


class Indicators:
    """
        Indicators are stored along
    """

    def __init__(self, candles, users, cache_manager):
        # Object manages and serves price and candle data.
        self.candles = candles

        # A connection to an object that handles user data.
        self.users = users

        # A connection to an object that handles all data.
        self.cache_manager = cache_manager

        # Cache for storing instantiated indicator objects
        cache_manager.create_cache(
            name='indicators',
            cache_type='table',
            size_limit=100,
            eviction_policy='deny',
            default_expiration=dt.timedelta(days=1),
            columns=['creator', 'name', 'visible', 'kind', 'source', 'properties', 'ref']
        )

        # Cache for storing calculated indicator data
        cache_manager.create_cache('indicator_data', cache_type='row', size_limit=100,
                                   default_expiration=dt.timedelta(days=7), eviction_policy='evict')

        # Cache for storing display properties indicators
        cache_manager.create_cache('user_display_properties', cache_type='row',
                                   size_limit=100,
                                   default_expiration=dt.timedelta(days=1),
                                   eviction_policy='evict')

        # Available indicator types and classes from a global indicators_registry.
        self.indicator_registry = indicators_registry

        # Enums values to use with Bolenger-bands.
        self.MV_AVERAGE_ENUM = {'SMA': 0, 'EMA': 1, 'WMA': 2, 'DEMA': 3, 'TEMA': 4,
                                'TRIMA': 5, 'KAMA': 6, 'MAMA': 7, 'T3': 8}

    def get_available_indicator_types(self) -> list:
        """Returns a list of all available indicator types."""
        return list(self.indicator_registry.keys())

    def get_indicator_list(self, username: str = None, user_id: int = None, only_enabled: bool = False) -> Dict[str, Dict[str, Any]]:
        """
        Returns a dictionary of all indicators available to this user.
        If only_enabled is True, returns only indicators marked as visible.

        :param user_id:
        :param username: str - The user_name of the user.
        :param only_enabled: bool - If True, return only indicators marked as visible.
        :return: dict - A dictionary of indicator names as keys and their attributes as values.
        """
        if (not user_id) and (not username):
            raise ValueError(f"Must provide a valid user_name or user_id")

        if not user_id:
            user_id = self.users.get_id(username)

        if not user_id:
            raise ValueError(f"Invalid user_name: {username}")

        # Fetch indicators based on visibility status
        if only_enabled:
            indicators_df = self.cache_manager.get_rows_from_datacache('indicators',
                                                                       [('creator', str(user_id)), ('visible', True)])
        else:
            indicators_df = self.cache_manager.get_rows_from_datacache('indicators', [('creator', str(user_id))])

        # Check if the DataFrame is empty
        if indicators_df is None or indicators_df.empty:
            return {}  # Return an empty dictionary if no indicators are found

        result = {}

        # Iterate over the rows and construct the result dictionary
        for _, row in indicators_df.iterrows():
            # Ensure that row['properties'] is a dictionary
            properties = row.get('properties', {})
            properties = json.loads(properties) if isinstance(properties, str) else properties

            # Deserialize the 'source' field if it is a JSON string
            source = row.get('source', {})
            source = json.loads(source) if isinstance(source, str) else source

            # Construct the result dictionary for each indicator
            result[row['name']] = {
                'type': row['kind'],
                'visible': bool(row['visible']),
                'source': source,
                **properties  # Merge in all properties from the properties field
            }

        return result

    def toggle_indicators(self, user_id: int, indicator_names: list) -> None:
        """
        Set the visibility of indicators for a user.

        :param user_id: The id of the user.
        :param indicator_names: List of indicator names to set as visible.
        :return: None
        """
        indicators = self.cache_manager.get_rows_from_datacache('indicators', [('creator', str(user_id))])

        if indicators.empty:
            return

        # Set visibility for all indicators off
        self.cache_manager.modify_multiple_datacache_items(
            cache_name='indicators',
            filter_vals=[('creator', str(user_id))],
            field_names=('visible',),
            new_values=(False,),
            overwrite='name'
        )

        # Set visibility for the specified indicators on
        if indicator_names:
            self.cache_manager.modify_multiple_datacache_items(
                cache_name='indicators',
                filter_vals=[('creator', str(user_id)), ('name', indicator_names)],
                field_names=('visible',),
                new_values=(True,),
                overwrite='name'
            )

    def edit_indicator(self, user_name: str, params: dict):
        """
        Edits an existing indicator's properties.
        :param user_name: The name of the user.
        :param params: The updated properties of the indicator.
        """
        try:
            indicator_name = params.get('name')
            if not indicator_name:
                raise ValueError("Indicator name is required for editing.")

            # Get user ID and retrieve the indicator
            user_id = self.users.get_id(user_name)
            indicator = self.cache_manager.get_rows_from_datacache(
                'indicators',
                [('name', indicator_name), ('creator', str(user_id))]
            )

            if indicator.empty:
                raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.")

            # Validate and update 'properties'
            new_properties = params.get('properties')
            if new_properties is not None:
                if not isinstance(new_properties, dict):
                    raise ValueError("'properties' must be a dictionary.")
                # Serialize the dictionary to a JSON string
                new_properties_json = json.dumps(new_properties, sort_keys=True)
                # Retrieve existing properties as JSON string
                existing_properties_str = indicator['properties'].iloc[0]

                if existing_properties_str != new_properties_json:
                    self.cache_manager.modify_datacache_item(
                        'indicators',
                        [('creator', str(user_id)), ('name', indicator_name)],
                        ('properties',),  # field_names as a single-element tuple
                        (new_properties_json,),  # new_values as a single-element tuple
                        overwrite='name'  # overwrite remains as a keyword argument
                    )

            # Validate and update 'source'
            new_source = params.get('source')
            if new_source is not None:
                if not isinstance(new_source, dict):
                    raise ValueError("'source' must be a dictionary.")
                # Serialize the dictionary to a JSON string
                new_source_json = json.dumps(new_source, sort_keys=True)
                # Retrieve existing source as JSON string
                existing_source_str = indicator['source'].iloc[0] if 'source' in indicator else None

                if existing_source_str != new_source_json and new_source_json is not None:
                    self.cache_manager.modify_datacache_item(
                        'indicators',
                        [('creator', str(user_id)), ('name', indicator_name)],
                        ('source',),  # field_names as a single-element tuple
                        (new_source_json,),  # new_values as a single-element tuple
                        overwrite='name'  # overwrite remains as a keyword argument
                    )

            # Validate and update 'visible'
            current_visible = str(indicator['visible'].iloc[0]).lower() in ['true', '1', 't', 'yes']
            new_visible = bool(params.get('visible'))

            if current_visible != new_visible:
                self.cache_manager.modify_datacache_item(
                    'indicators',
                    [('creator', str(user_id)), ('name', indicator_name)],
                    ('visible',),  # field_names as a single-element tuple
                    (new_visible,),  # new_values as a single-element tuple
                    overwrite='name'  # overwrite remains as a keyword argument
                )
        except Exception as e:
            raise e

    def new_indicator(self, user_name: str, params) -> None:
        """
        Appends a new indicator to a user-specific collection of Indicator definitions.

        :param user_name: The name of the user triggering the action.
        :param params: The request parameters containing indicator information.
        :return: None
        """
        indcr = params['name']
        indtyp = params['type']

        # Validate indicator name and type
        if not indcr:
            raise ValueError("Indicator name is required.")
        if indtyp not in indicators_registry:
            raise ValueError("Unsupported indicator type.")

        # Create a dictionary of properties from the values in request form.
        source = params['source']

        # Validate properties (assuming properties is a dictionary)
        properties = {}
        if params['properties']:
            properties = json.loads(params['properties'])

        # Create indicator.
        self.create_indicator(creator=user_name, name=indcr, kind=indtyp, source=source, properties=properties)

    def process_indicator(self, indicator, num_results: int = 1) -> pd.DataFrame | None:
        """
        Trigger execution of the indicator's analysis against an updated source.

        :param indicator: A named tuple or dict containing indicator data.
        :param num_results: The number of results being requested.
        :return: The results of the indicator analysis as a DataFrame.
        """
        username = self.users.get_username(indicator.creator)
        src = indicator.source

        # Deserialize src if it's a string
        if isinstance(src, str):
            try:
                src = json.loads(src)
            except json.JSONDecodeError as e:
                print(f"Error decoding JSON for indicator '{indicator.name}': {e}")
                return None  # or handle the error as appropriate

        symbol, timeframe, exchange_name = src['market'], src['timeframe'], src['exchange']

        # Retrieve necessary details to instantiate the indicator
        name = indicator.name
        kind = indicator.kind
        properties = json.loads(indicator.properties)

        # Adjust num_results to account for the lookup period if specified in the indicator properties.
        if 'period' in properties:
            num_results += int(properties['period'])

        # Request the data from the defined source.
        data = self.candles.get_last_n_candles(num_candles=num_results,
                                               asset=symbol, timeframe=timeframe,
                                               exchange=exchange_name, user_name=username)

        # Instantiate the indicator object based on the kind
        indicator_class = self.indicator_registry[kind]
        indicator_obj = indicator_class(name=name, indicator_type=kind, properties=properties)

        # Run the calculate method of the indicator
        return indicator_obj.calculate(candles=data, user_name=username, num_results=num_results)

    def get_indicator_data(self, user_name: str, source: dict = None,
                           visible_only: bool = True, start_ts: float = None,
                           num_results: int = 1000) -> Optional[Dict[str, Any]]:
        """
        Loop through enabled indicators in a user's watch-list. Run the appropriate
        update function and return a dictionary containing all the results.

        :param user_name: The name of the user making the request.
        :param source: Pass in a source definition to return only results against a particular source.
        :param visible_only: Returns only results marked visible.
        :param start_ts: The timestamp to begin the analysis at. (Not implemented yet)
        :param num_results: The number of results requested.
        :return: A dictionary of timestamped data returned from each indicator indexed by the indicator's name,
                 or None if no indicators matched the query.
        """
        if start_ts:
            print("Warning: start_ts has not been implemented in get_indicator_data()!")

        user_id = self.users.get_id(user_name=user_name)

        visible = True if visible_only else False

        # Filter the indicators based on the query.
        indicators = self.cache_manager.get_rows_from_datacache('indicators',
                                                                [('creator', str(user_id)), ('visible', visible)])

        # Return None if no indicators matched the query.
        if indicators.empty:
            return None

        if source:
            # Convert 'source' column to dictionaries if they are strings
            indicators['source'] = indicators['source'].apply(lambda x: json.loads(x) if isinstance(x, str) else x)

            # Extract relevant fields from the source's market
            source_timeframe = source.get('market', {}).get('timeframe')
            source_exchange = source.get('market', {}).get('exchange')
            source_symbol = source.get('market', {}).get('market')

            # Extract fields from indicators['source'] and compare directly
            mask = (indicators['source'].apply(lambda s: s.get('timeframe')) == source_timeframe) & \
                   (indicators['source'].apply(lambda s: s.get('exchange')) == source_exchange) & \
                   (indicators['source'].apply(lambda s: s.get('market')) == source_symbol)

            # Convert source['market'] to a string for comparison
            # source_market_str = json.dumps(source.get('market', {}))
            # # Compare 'source' as strings
            # mask = indicators['source'].apply(
            #     lambda s: (
            #         json.dumps(json.loads(s).get('market', {})) == source_market_str
            #         if isinstance(s, str) else
            #         json.dumps(s.get('market', {})) == source_market_str
            #     )
            # )

            # Filter the DataFrame using the mask
            indicators = indicators[mask]

        # If no indicators match the filtered source, return None.
        if indicators.empty:
            return None

        # Process each indicator, convert DataFrame to JSON-serializable format, and collect the results
        json_ready_results = {}

        for indicator in indicators.itertuples(index=False):
            indicator_results = self.process_indicator(indicator=indicator, num_results=num_results)

            # Convert DataFrame to list of dictionaries if necessary
            if isinstance(indicator_results, pd.DataFrame):
                json_ready_results[indicator.name] = indicator_results.to_dict(orient='records')
            else:
                json_ready_results[indicator.name] = indicator_results  # If not a DataFrame, leave as is

        return json_ready_results

    def get_latest_indicator_data(self, user_name: str, indicator_name: str, num_results: int = 1) -> Optional[
        Dict[str, Any]]:
        """
        Retrieves the latest data points for a specific indicator for a given user.

        :param user_name: The name of the user.
        :param indicator_name: The name of the indicator.
        :param num_results: Number of latest results to fetch (default is 1).
        :return: A dictionary containing the latest indicator data, or None if not found.
        """
        try:
            # Step 1: Get User ID
            user_id = self.users.get_id(user_name=user_name)
            if not user_id:
                raise ValueError(f"Invalid user_name: '{user_name}'")

            # Step 2: Retrieve the Specific Indicator
            indicators = self.cache_manager.get_rows_from_datacache(
                'indicators',
                [('creator', str(user_id)), ('name', indicator_name)]
            )

            if indicators.empty:
                print(f"Indicator '{indicator_name}' not found for user '{user_name}'.")
                return None  # Indicator not found

            # Assuming indicator names are unique per user, take the first match
            indicator = indicators.iloc[0]

            # Step 3: Process the Indicator to Get Data
            indicator_result = self.process_indicator(indicator=indicator, num_results=num_results)

            # Step 4: Extract the Latest Data Points
            if isinstance(indicator_result, pd.DataFrame):
                latest_data = indicator_result.tail(num_results).to_dict(orient='records')
                return {indicator_name: latest_data}
            elif isinstance(indicator_result, dict):
                return {indicator_name: indicator_result}
            else:
                print(f"Unexpected data format for indicator '{indicator_name}'.")
                return None

        except Exception as e:
            print(f"Error retrieving latest data for indicator '{indicator_name}': {e}")
            return None

    def delete_indicator(self, indicator_name: str, user_name: str) -> None:
        """
            Remove the indicator by name
            :param user_name: username
            :param indicator_name: The name of the indicator to remove.
            :return: None
        """
        if not indicator_name:
            raise ValueError("No indicator name provided.")

        # Get the user ID to filter the indicators belonging to the user
        user_id = self.users.get_id(user_name)

        identifying_values = [('name', indicator_name), ('creator', str(user_id))]
        self.cache_manager.remove_row_from_datacache(cache_name='indicators', filter_vals=identifying_values)

    def create_indicator(self, creator: str, name: str, kind: str,
                         source: dict, properties: dict, visible: bool = True):
        """
        Created an instance of the indicator specified in the args.
        Initialise it with properties and store its reference in a dataframe.
        For convenience, the indicator is indexed by name, type, and source info.

        :param creator: The name of the user creating the indicator.
        :param name: The name of the indicator being created.
        :param kind: The kind of indicator being created.
        :param source: The source of data that the indicator analyzes.
        :param properties: Properties and values to pass into the initializer.
        :param visible: Whether to display it in the chart view.
        :return: None
        """

        creator_id = self.users.get_id(creator)
        # Check if an indicator with the same name already exists
        indicators = self.cache_manager.get_rows_from_datacache('indicators', [('name', name),
                                                                               ('creator', str(creator_id))])

        if not indicators.empty:
            raise ValueError(f"Indicator '{name}' already exists. Please choose a different name.")

        if kind not in self.indicator_registry:
            raise ValueError(f"Requested an unsupported type of indicator: ({kind})")

        # Instantiate the indicator class from the registry
        IndicatorClass = indicators_registry[kind]
        indicator_instance = IndicatorClass(name=name, indicator_type=kind, properties=properties)

        # Sanitize source and properties by converting them to JSON strings
        sanitized_source = json.dumps(source)  # Converts the dictionary to a JSON string
        sanitized_properties = json.dumps(indicator_instance.properties)  # Same for properties

        # Add the new indicator to a pandas dataframe.
        row_data = pd.DataFrame([{
            'creator': creator_id,
            'name': name,
            'kind': kind,
            'visible': bool(visible),
            'source': sanitized_source,
            'properties': sanitized_properties
        }])
        self.cache_manager.insert_df_into_datacache(df=row_data, cache_name="indicators", skip_cache=False)

    # def update_indicators(self, user_name):
    #     """
    #         This does nothing except add a default value.
    #         :return: None
    #     """
    #     return self.get_indicator_data(user_name=user_name, num_results=1)
