import os
import logging
from getpass import getpass
import dotenv
import ccxt
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
from time import sleep
import time
from tradingview_ta import TA_Handler, Interval, Exchange
import mplfinance as mpf

# Load .env file if it exists

dotenv.load_dotenv()

# Configuration for logging
log_dir = 'logs'
if not os.path.exists(log_dir):
    os.makedirs(log_dir)

logging.basicConfig(
    filename=os.path.join(log_dir, 'trading_bot.log'),
    level=logging.INFO,
    format='%(asctime)s:%(levelname)s:%(message)s'
)

# Log to console as well
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(message)s'))
logging.getLogger('').addHandler(console_handler)

# Function to get environment variables from .env or prompt the user
def get_env_variable(var_name, prompt_text=None):
    value = os.getenv(var_name)
    if not value and prompt_text:
        try:
            value = getpass(prompt_text)
        except Exception as e:
            logging.error(f"Error getting environment variable {var_name}: {str(e)}")
            raise
    return value

class CryptoTradingBot:
    def __init__(self, exchange_name, api_key, api_secret, timeframe='1h', balance=10000, max_risk=0.02, paper_trading=True):
        """
        ###Initialize the CryptoTradingBot with configuration parameters.

        ##Arguments:
        #exchange_name - Name of the exchange (e.g., 'binance')
        #api_key - API key
        #api_secret - API secret
        #timeframe - Timeframe for OHLCV data (default '1h')
        #balance - Initial balance for trading (default 10000)
        #max_risk - Maximum risk per trade as a fraction (default 0.02)
        #paper_trading - Flag for whether to use paper trading (default True)
        """
        self.exchange = getattr(ccxt, exchange_name)({
            'apiKey': api_key,
            'secret': api_secret,
        })
        self.timeframe = timeframe
        self.balance = balance
        self.max_risk = max_risk
        self.paper_trading = paper_trading
        self.symbols = self.get_usdt_symbols()

    def get_usdt_symbols(self, min_volume=None, price_range=None, show_all=False):
        """
        ###Retrieve a list of all trading symbols ending with '/USDT'. Optionally filter by minimum volume and price range.

        #Arguments:
        #min_volume - Minimum trading volume to filter symbols (default None)
        #price_range - Tuple containing min and max price to filter symbols (default None)
        #show_all - Flag to show all symbols without filtering (default False)

        #Returns:
        #List of symbols in the format 'SYMBOL/USDT' that match the filter criteria.
        """
        try:
            markets = self.exchange.load_markets()
            usdt_symbols = [symbol for symbol in markets if symbol.endswith('/USDT')]
            if not show_all:
                if min_volume:
                    usdt_symbols = [symbol for symbol in usdt_symbols if self.exchange.fetch_ticker(symbol)['quoteVolume'] >= min_volume]
                if price_range:
                    usdt_symbols = [symbol for symbol in usdt_symbols if price_range[0] <= self.exchange.fetch_ticker(symbol)['last'] <= price_range[1]]
            logging.info("Successfully fetched and filtered USDT symbols.")
            return usdt_symbols
        except (ccxt.ExchangeError, ccxt.NetworkError) as e:
            logging.error(f"Failed to fetch USDT symbols: {e}")
            return []

    def fetch_data(self, symbol, limit=500, retries=3, backoff_factor=1.0):
        """
        #Fetch OHLCV data for a given trading symbol with retries and exponential backoff.

        #Arguments:
        #symbol - Trading symbol (e.g., 'BTC/USDT')
        #limit - Number of data points to fetch (default 500)
        #retries - Number of attempts in case of failure (default 3)
        #backoff_factor - Factor for exponential backoff (default 1.0)

        #Returns:
        #DataFrame containing OHLCV data.
        """
        for attempt in range(retries):
            try:
                bars = self.exchange.fetch_ohlcv(symbol, timeframe=self.timeframe, limit=limit)
                df = pd.DataFrame(bars, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
                df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
                if self.validate_data(df):
                    logging.info(f"Successfully fetched data for {symbol} on attempt {attempt + 1}.")
                    return df
                else:
                    logging.warning(f"Validation failed for data fetched for {symbol}. Attempt {attempt + 1}/{retries}.")
            except (ccxt.NetworkError, ccxt.ExchangeNotAvailable) as e:
                logging.error(f"Network/Availability error while fetching data for {symbol}: {e}. Retrying in {backoff_factor * (2 ** attempt)} seconds.")
            except ccxt.ExchangeError as e:
                logging.error(f"Exchange error while fetching data for {symbol}: {e}. Retrying in {backoff_factor * (2 ** attempt)} seconds.")
            except Exception as e:
                logging.error(f"Unexpected error while fetching data for {symbol}: {e}. Retrying in {backoff_factor * (2 ** attempt)} seconds.")
            sleep(backoff_factor * (2 ** attempt))  # Exponential backoff
        logging.error(f"Failed to fetch data for {symbol} after {retries} attempts.")
        return None

    def fetch_real_time_data(self, symbol):
        """
        #Fetch real-time ticker data for a given trading symbol.

        #Arguments:
        #symbol - Trading symbol (e.g., 'BTC/USDT')

        #Returns:
        #Dictionary containing real-time ticker data.
        """
        try:
            ticker = self.exchange.fetch_ticker(symbol)
            logging.info(f"Successfully fetched real-time data for {symbol}.")
            return ticker
        except ccxt.NetworkError as e:
            logging.error(f"Network error while fetching real-time data for {symbol}: {e}")
            return None
        except ccxt.ExchangeError as e:
            logging.error(f"Exchange error while fetching real-time data for {symbol}: {e}")
            return None
        except Exception as e:
            logging.error(f"Unexpected error while fetching real-time data for {symbol}: {e}")
            return None

    def fetch_real_time_data_all(self):
        """
        #Fetch real-time ticker data for all USDT symbols concurrently.

        #Returns:
        #Dictionary containing real-time ticker data for all USDT symbols.
        """
        results = {}
        with ThreadPoolExecutor(max_workers=5) as executor:
            future_to_symbol = {executor.submit(self.fetch_real_time_data, symbol): symbol for symbol in self.symbols}
            for future in as_completed(future_to_symbol):
                symbol = future_to_symbol[future]
                try:
                    data = future.result()
                    if data is not None:
                        results[symbol] = data
                except Exception as e:
                    logging.error(f"Error fetching real-time data for {symbol}: {e}")
        return results

    def fetch_technical_indicators(self, symbol):
        """
        #Fetch technical indicators from TradingView for a given trading symbol.

        #Arguments:
        #symbol - Trading symbol (e.g., 'BTC/USDT')

        #Returns:
        #Dictionary containing technical indicators.
        """
        handler = TA_Handler(
            symbol=symbol.replace('/', ''),
            screener="crypto",
            exchange="BINANCE",
            interval=Interval.INTERVAL_1_HOUR
        )
        analysis = handler.get_analysis()
        return analysis.indicators

    def plot_ohlcv_with_indicators(self, df, symbol):
        """
        #Plot OHLCV data with technical indicators.

        #Arguments:
        #df - DataFrame containing OHLCV data
        #symbol - Trading symbol (e.g., 'BTC/USDT')
        """
        mpf.plot(df, type='candle', style='charles', title=symbol, ylabel='Price')

    @staticmethod
    def validate_data(df):
        """
        #Validate the fetched data to ensure its integrity.

        #Arguments:
        #df - DataFrame containing OHLCV data

        #Returns:
        #Boolean indicating whether the data is valid.
        """
        if df.isnull().values.any():
            return False
        if df.empty:
            return False
        return True

    def fetch_data_concurrently(self, symbols, limit=500, max_workers=5):
        """
        #Fetch data for multiple symbols concurrently.

        #Arguments:
        #symbols - List of trading symbols
        #limit - Number of data points to fetch per symbol (default 500)
        #max_workers - Maximum number of worker threads (default 5)

        #Returns:
        #Dictionary of DataFrames containing OHLCV data.
        """
        results = {}
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            future_to_symbol = {executor.submit(self.fetch_data, symbol, limit): symbol for symbol in symbols}
            for future in as_completed(future_to_symbol):
                symbol = future_to_symbol[future]
                try:
                    data = future.result()
                    if data is not None:
                        results[symbol] = data
                except Exception as e:
                    logging.error(f"Error fetching data for {symbol}: {e}")
        return results

if __name__ == "__main__":
    # Example usage, we’ll use these variables later in the rest of the bot
    binance_api_key = get_env_variable("BINANCE_API_KEY", "Binance API Key: ")
    binance_api_secret = get_env_variable("BINANCE_API_SECRET", "Binance API Secret: ")

    # Create an instance of CryptoTradingBot
    bot = CryptoTradingBot(
        exchange_name='binance',
        api_key=binance_api_key,
        api_secret=binance_api_secret,
        timeframe='1h',  # User can change this to any supported timeframe (e.g., ‘5m’, ‘1d’)
    )

    # Fetch and display USDT trading symbols with optional volume and price filtering
    usdt_symbols = bot.get_usdt_symbols(min_volume=1000, price_range=(0.01, 100), show_all=False)
    print("Filtered USDT Trading Symbols:", usdt_symbols)

    # Fetch and display historical OHLCV data for a sample symbol
    sample_data = bot.fetch_data('BTC/USDT')
    if sample_data is not None:
        print(sample_data.head())
        bot.plot_ohlcv_with_indicators(sample_data, 'BTC/USDT')

    # Fetch and display real-time data for all USDT symbols
    real_time_data_all = bot.fetch_real_time_data_all()
    for symbol, data in real_time_data_all.items():
        print(f"Real-time data for {symbol}:", data)

    # Concurrently fetch historical data for multiple symbols
    multiple_data = bot.fetch_data_concurrently(usdt_symbols[:5])
    for symbol, df in multiple_data.items():
        print(f"Data for {symbol}:\n", df.head())

Wat does this code do