from pybacktestchain.data_module import FirstTwoMoments
from pybacktestchain.broker import Backtest, StopLoss
from pybacktestchain.blockchain import load_blockchain
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
import webbrowser
from threading import Timer
import dash
import plotly.express as px
from dash import Dash, dcc, html, Input, Output
import dash_bootstrap_components as dbc
import yfinance as yf
from io import StringIO
import os
import threading

# List of S&P 500 tickers (manually retrieved)
SP500_TICKERS = [
    "AAPL", "MSFT", "AMZN", "GOOGL", "META", "BRK-B", "JNJ", "V", "PG", "NVDA", "TSLA",
    "JPM", "UNH", "HD", "DIS", "PYPL", "MA", "NFLX", "KO", "PFE", "ABT", "CSCO", "XOM",
    "PEP", "CMCSA", "ADBE", "T", "MRK", "INTC", "CRM", "WMT", "NKE", "VZ", "TMO", "MDT",
    "COST", "AMGN", "LLY", "QCOM", "DHR", "ACN", "HON", "MCD", "BMY", "AVGO", "TXN", "UNP",
    "NEE", "PM", "LOW", "MS", "LIN", "INTU", "RTX", "SCHW", "IBM", "NOW", "CAT", "SPGI",
    "GS", "BLK", "BKNG", "DE", "EL", "CVX", "LMT", "AXP", "MDLZ", "ISRG", "ZTS", "GE",
    "ADP", "GILD", "ADI", "TGT", "CB", "MMC", "SYK", "CME", "PLD", "TMUS", "CCI", "WM",
    "NSC", "ETN", "ICE", "EW", "ITW", "AMT", "MU", "CL", "AON", "VRTX", "SO", "BSX",
    "NOC", "ATVI", "REGN", "FISV", "SHW", "D", "F", "MRNA", "ORCL", "PGR", "HUM", "COP",
    "KMB", "DG", "FTNT", "BK", "CMI", "ALL", "STZ", "AEP", "KHC", "SPG", "GM", "LRCX",
    "LHX", "ROST", "EA", "PAYX", "APH", "TT", "WELL", "VLO", "VRSK", "CTAS", "MNST"]

#Here df that we will use is the last dataframe obtained above from the loop
#where we looped on the block of the blockchains

#function that compute the pnl of the folder and stocks
def compute_pnl(df):
    df['Average_Buy_Price'] = df.groupby('Ticker', group_keys=False)['Price'].transform(
        lambda x: x.expanding().mean()
    )
    df['Transaction_PnL'] = np.where(
        df['Action'] == 'SELL',
        (df['Price'] - df['Average_Buy_Price']) * df['Quantity'],
        0
    )
    df['Cumulative_PnL'] = df.groupby('Date')['Transaction_PnL'].cumsum()
    overall_pnl = df.groupby('Date')['Cumulative_PnL'].sum().reset_index(name='Overall_PnL')
    stock_pnl = df.groupby(['Date', 'Ticker'])['Cumulative_PnL'].sum().reset_index()
    return overall_pnl, stock_pnl

#function that compute the returns for the portfolio and stocks
def compute_returns(df):
    df['Average_Buy_Price'] = df.groupby('Ticker', group_keys=False)['Price'].transform(
        lambda x: x.expanding().mean()
    )
    df['Stock_Value'] = df['Quantity'] * df['Price']
    df['Portfolio_Value'] = df['Cash'] + df.groupby('Date', group_keys=False)['Stock_Value'].transform('sum')
    df['Daily_Return'] = df['Portfolio_Value'].pct_change().fillna(0)

    portfolio_returns = df[['Date', 'Daily_Return']].drop_duplicates()
    stock_returns = df[['Date', 'Ticker', 'Daily_Return']].drop_duplicates()

    portfolio_sharpe = sharpe_ratio(portfolio_returns['Daily_Return'])
    stock_sharpes = df.groupby('Ticker')['Daily_Return'].apply(sharpe_ratio).reset_index()
    stock_sharpes.columns = ['Ticker', 'Sharpe_Ratio']

    return portfolio_returns, stock_returns, portfolio_sharpe, stock_sharpes

#function that compute the sharp-ratio using Risk-free IR equal to 1%
def sharpe_ratio(returns, risk_free_rate=0.01):
    excess_returns = returns - risk_free_rate / 252
    mean_excess_return = excess_returns.mean()
    std_excess_return = excess_returns.std()
    if std_excess_return == 0:
        return 0
    return mean_excess_return / std_excess_return



def fetch_and_compute_indicators(tickers):
    """
    Fetch historical data and compute moving averages and Bollinger Bands for a list of tickers.
    Handles NaN values generated by rolling calculations.
    """
    # Define the date range
    today = datetime.today()
    end_date = (today - timedelta(days=1)).strftime('%Y-%m-%d')  # Yesterday's date
    start_date = (today - timedelta(days=365)).strftime('%Y-%m-%d')  # 1 year ago from today

    # Initialize a list to store processed DataFrames
    processed_data = []

    for ticker in tickers:
        try:
            # Fetch historical data using Ticker object and history() method
            stock = yf.Ticker(ticker)
            data = stock.history(start=start_date, end=end_date, auto_adjust=False, actions=False)

            # If data is empty, log and skip
            if data.empty:
                print(f"No data available for {ticker}")
                continue

            # Reset index to have a clean DataFrame
            data = data.reset_index()

            # Compute moving averages
            data['MA_15'] = data['Close'].rolling(window=15).mean()
            data['MA_30'] = data['Close'].rolling(window=30).mean()
            data['MA_45'] = data['Close'].rolling(window=45).mean()

            # Compute Bollinger Bands
            data['BB_Mid'] = data['Close'].rolling(window=20).mean()
            data['BB_Std'] = data['Close'].rolling(window=20).std()  # Rolling standard deviation
            data['BB_Upper'] = data['BB_Mid'] + 2 * data['BB_Std']  # Upper Band
            data['BB_Lower'] = data['BB_Mid'] - 2 * data['BB_Std']  # Lower Band

            # Add Ticker column for identification
            data['Ticker'] = ticker

            #  Drop rows with NaN values (useful if analysis doesn't require them)
            data.dropna(inplace=True)
    

            # Drop temporary columns used for computation 
            data.drop(columns=['BB_Std'], inplace=True)

            # Append processed data to the list
            processed_data.append(data)

        except Exception as e:
            # Log any error that occurs during processing
            print(f"Error processing {ticker}: {e}")

    # Combine all processed data into a single DataFrame
    if processed_data:
        return pd.concat(processed_data, ignore_index=True)
    else:
        # Return an empty DataFrame if no data was processed
        return pd.DataFrame()

def fetch_and_compute_returns(tickers):
    """
    Fetch historical data and compute daily returns for a list of tickers.
    Returns a DataFrame indexed by date, with columns as tickers and values as returns.
    """
    # Define the date range
    today = datetime.today()
    end_date = (today - timedelta(days=1)).strftime('%Y-%m-%d')  # Yesterday's date
    start_date = (today - timedelta(days=365)).strftime('%Y-%m-%d')  # 1 year ago from today

    # Initialize a list to store processed DataFrames
    processed_data = []

    for ticker in tickers:
        try:
            # Fetch historical data
            stock = yf.Ticker(ticker)
            data = stock.history(start=start_date, end=end_date, auto_adjust=False, actions=False)

            # If data is empty, log and skip
            if data.empty:
                print(f"No data available for {ticker}")
                continue

            # Compute daily returns
            data['Return'] = data['Close'].pct_change()

            # Select only the returns and reset index
            returns_data = data[['Return']].reset_index()
            returns_data.rename(columns={'Return': ticker}, inplace=True)

            # Append to the processed data list
            processed_data.append(returns_data.set_index('Date')[ticker])

        except Exception as e:
            print(f"Error processing {ticker}: {e}")

    # Combine all returns into a single DataFrame
    if processed_data:
        combined_returns = pd.concat(processed_data, axis=1)
        combined_returns.dropna(how='all', inplace=True)  # Drop rows where all tickers are NaN
        return combined_returns
    else:
        return pd.DataFrame()


def compute_correlation_matrix(df):
    """
    Compute the correlation matrix for selected stocks.
    """
    pivot_df = df.pivot(index='Date', columns='Ticker', values='Price')  # Pivot to have Tickers as columns
    pivot_df = pivot_df.fillna(0)  # Drop columns with any NaN values
    correlation_matrix = pivot_df.corr()  # Compute correlations
    return correlation_matrix


# Dash app encapsulated in a function
def run_app(df):
    # Initialize Dash app
    app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

    # Layout
    app.layout = dbc.Container([
        dbc.Row([
            dbc.Col(
                html.H1(
                    "Dashboard Projet Python Backtest",
                    className="text-center my-4"  # Classes Bootstrap pour alignement et marges
                ),
                width=12
            )
        ]),  # Ajout du titre principal
        dbc.Row([
            dbc.Col([
                dcc.Dropdown(
                    id='stock-selector',
                    options=[{'label': ticker, 'value': ticker} for ticker in df['Ticker'].unique()],
                    value=df['Ticker'].unique(),
                    multi=True,
                    placeholder="Select stocks to display"
                )
            ], width=12)
        ], className="mb-3"),

        dbc.Row([
            dbc.Col(dcc.Graph(id='overall-pnl-chart'), width=6),
            dbc.Col(dcc.Graph(id='stock-pnl-chart'), width=6),
        ]),

        dbc.Row([dbc.Col(dcc.Graph(id='returns-chart'), width=12)]),

        dbc.Row([
            dbc.Col([
                html.H5("Sharpe Ratios", className="text-center"),
                html.Table(id='sharpe-ratio-table', className="table table-striped table-bordered")
            ], width=12)
        ], className="mb-4"),

        html.Hr(),  # Separator
        html.H3("Statistical Indicators on Past Year Data", className="text-center mt-4 mb-4"),
        dbc.Row([
            dbc.Col([
                dcc.Dropdown(
                    id='sp500-stock-selector',
                    options=[{'label': ticker, 'value': ticker} for ticker in SP500_TICKERS],
                    multi=True,
                    value=df['Ticker'].unique(),
                    placeholder="Select S&P 500 stocks (max 10)",
                    maxHeight=300  # Limit the dropdown height for better UX
                )
            ], width=12)
        ], className="mb-3"),
        dbc.Row(id='sp500-dynamic-indicator-graphs', className="gy-4"),

        html.Hr(),  # Separator
        html.H3("Correlation Heatmap", className="text-center mt-4 mb-4"),
        dbc.Row([dbc.Col(dcc.Graph(id='correlation-heatmap'), width=12)])
    ], fluid=True)

    # Callbacks
    @app.callback(
        Output('overall-pnl-chart', 'figure'),
        Input('stock-selector', 'value')
    )
    def update_pnl_chart(selected_stocks):
        filtered_df = df[df['Ticker'].isin(selected_stocks)]
        overall_pnl, _ = compute_pnl(filtered_df)
        fig = px.line(
            overall_pnl,
            x='Date',
            y='Overall_PnL',
            title="Overall Portfolio PnL Over Time",
            labels={'Overall_PnL': 'PnL', 'Date': 'Date'}
        )
        return fig

    @app.callback(
        Output('stock-pnl-chart', 'figure'),
        Input('stock-selector', 'value')
    )
    def update_stock_pnl_chart(selected_stocks):
        filtered_df = df[df['Ticker'].isin(selected_stocks)]
        _, stock_pnl = compute_pnl(filtered_df)
        fig = px.line(
            stock_pnl,
            x='Date',
            y='Cumulative_PnL',
            color='Ticker',
            title="Stock PnL Over Time",
            labels={'Cumulative_PnL': 'PnL', 'Ticker': 'Stock'}
        )
        return fig

    @app.callback(
        Output('returns-chart', 'figure'),
        Input('stock-selector', 'value')
    )
    def update_returns_chart(selected_stocks):
        filtered_df = df[df['Ticker'].isin(selected_stocks)]
        _, stock_returns, _, _ = compute_returns(filtered_df)
        fig = px.line(
            stock_returns,
            x='Date',
            y='Daily_Return',
            color='Ticker',
            title="Daily Returns by Stock",
            labels={'Daily_Return': 'Daily Return', 'Date': 'Date'}
        )
        return fig

    @app.callback(
        Output('sharpe-ratio-table', 'children'),
        Input('stock-selector', 'value')
    )
    def update_sharpe_ratios(selected_stocks):
        filtered_df = df[df['Ticker'].isin(selected_stocks)]
        _, _, portfolio_sharpe, stock_sharpes = compute_returns(filtered_df)
        table_header = [
            html.Thead(html.Tr([html.Th("Ticker"), html.Th("Sharpe Ratio")]))
        ]
        table_body = [
            html.Tr([html.Td("Portfolio"), html.Td(f"{portfolio_sharpe:.2f}")])
        ]
        for _, row in stock_sharpes.iterrows():
            table_body.append(html.Tr([html.Td(row['Ticker']), html.Td(f"{row['Sharpe_Ratio']:.2f}")]))
        return table_header + [html.Tbody(table_body)]

    # Callback to update S&P 500 technical indicators graphs
    @app.callback(
        Output('sp500-dynamic-indicator-graphs', 'children'),
        Input('sp500-stock-selector', 'value')
    )
    def update_sp500_dynamic_graphs(selected_stocks):
        if not selected_stocks:
            return [html.Div("No stocks selected.", style={'textAlign': 'center', 'padding': '20px'})]
        
        if len(selected_stocks) > 10:
            return [html.Div("Please select a maximum of 10 stocks.", style={'textAlign': 'center', 'padding': '20px'})]

        graphs = []
        stock_data = fetch_and_compute_indicators(selected_stocks)
        for ticker in selected_stocks:
            ticker_data = stock_data[stock_data['Ticker'] == ticker]
            fig = px.line(ticker_data, x='Date', y='Close', title=f"{ticker} - Technical Indicators")
            fig.add_scatter(x=ticker_data['Date'], y=ticker_data['MA_15'], mode='lines', name='MA 15')
            fig.add_scatter(x=ticker_data['Date'], y=ticker_data['MA_30'], mode='lines', name='MA 30')
            fig.add_scatter(x=ticker_data['Date'], y=ticker_data['MA_45'], mode='lines', name='MA 45')
            fig.add_scatter(x=ticker_data['Date'], y=ticker_data['BB_Upper'], mode='lines', name='Upper Band')
            fig.add_scatter(
                x=ticker_data['Date'], y=ticker_data['BB_Lower'], mode='lines', fill='tonexty',
                fillcolor='rgba(173,216,230,0.3)', name='Lower Band'
            )
            graphs.append(dbc.Col(dcc.Graph(figure=fig), width=6))
        
        return graphs

    @app.callback(
    Output('correlation-heatmap', 'figure'),
    Input('sp500-stock-selector', 'value')
    )
    def update_correlation_heatmap(selected_stocks):
        if not selected_stocks:
            return px.imshow(
                [[0]], 
                title="No Data Available", 
                labels={'color': 'Correlation'}
            )

        # Fetch the returns for the selected stocks
        returns_data = fetch_and_compute_returns(selected_stocks)

        # If no returns data is available, return an empty heatmap
        if returns_data.empty:
            return px.imshow(
                [[0]], 
                title="No Data Available", 
                labels={'color': 'Correlation'}
            )

        # Compute the correlation matrix
        correlation_matrix = returns_data.corr()

        # Create the heatmap
        fig = px.imshow(
        correlation_matrix,
        title="Correlation Heatmap of Returns",
        color_continuous_scale=px.colors.sequential.Blues,  # Use a vivid blue scale
        labels={'color': 'Correlation'},
        x=correlation_matrix.columns,
        y=correlation_matrix.columns
        )
        fig.update_layout(
            height=500,
            xaxis_title="Ticker",
            yaxis_title="Ticker",
            margin=dict(l=40, r=40, t=40, b=40),
            coloraxis_colorbar=dict(
                title="Correlation",
                tickvals=[0.2, 0.4, 0.6, 0.8, 1.0],  # Define tick marks for better scale readability
            )
        )
        fig.update_traces(
            zmin=0,  # Set minimum correlation value
            zmax=1   # Set maximum correlation value
        )

        return fig


    # Function to open the browser
    def open_browser():
        webbrowser.open_new("http://127.0.0.1:8050/")

    # Run the Dash app
    if __name__ == "__main__":
        Timer(1, open_browser).start()  # Open the browser after a 1-second delay
        app.run_server(debug=False, port=8050)


    

def main():
    # Logique principale ici
    verbose = False  # Set to True to enable logging, or False to suppress it

    # Exécution unique du backtest
    backtest = Backtest(
        initial_date=datetime(2019, 1, 1),
        final_date=datetime(2020, 1, 1),
        information_class=FirstTwoMoments,
        risk_model=StopLoss,
        name_blockchain='backtest',
        verbose=verbose
    )

    backtest.run_backtest()

    block_chain = load_blockchain('backtest')
    print(str(block_chain))
    # Check if the blockchain is valid
    print(block_chain.is_valid())

    # Préparer les données pour l'application Dash
    df_test = None
    for block in block_chain.chain:
        if block.name_backtest == "Genesis Block":  # Ignorer le bloc Genesis
            continue
        
        print(f"Processing block: {block.name_backtest}")
        
        try:
            # Convertir les données en DataFrame
            df_test = pd.read_csv(StringIO(block.data), delim_whitespace=True)
            print("Data as DataFrame:")
            print(df_test.head())
        except Exception as e:
            print(f"Error converting block data to DataFrame: {e}")
        print("-" * 80)
    
    return df_test

if __name__ == "__main__":
    # Exécutez la logique principale une seule fois
    df_test = main()

    # Lancer l'application Dash avec les données générées
    if df_test is not None:
        run_app(df_test)
    else:
        print("No valid data available to run the Dash app.")