"""Backtesting Model"""
__docformat__ = "numpy"

import logging
import warnings

import bt
import pandas as pd
import pandas_ta as ta
import yfinance as yf

from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import is_intraday
from openbb_terminal.common.technical_analysis import ta_helpers

logger = logging.getLogger(__name__)


@log_start_end(log=logger)
def get_data(symbol: str, start_date: str = "2019-01-01") -> pd.DataFrame:
    """Function to replace bt.get, gets Adjusted close of symbol using yfinance.

    Parameters
    ----------
    symbol: str
        Ticker to get data for
    start_date: str
        Start date in YYYY-MM-DD format

    Returns
    -------
    prices: pd.DataFrame
        Dataframe of Adj Close with columns = [ticker]
    """
    data = yf.download(symbol, start=start_date, progress=False)
    close_col = ta_helpers.check_columns(data, high=False, low=False)
    if close_col is None:
        return pd.DataFrame()
    prices = pd.DataFrame(data[close_col])
    prices.columns = [symbol]
    return prices


@log_start_end(log=logger)
def buy_and_hold(symbol: str, start_date: str, name: str = "") -> bt.Backtest:
    """Generates a buy and hold backtest object for the given ticker.

    Parameters
    ----------
    symbol: str
        Stock to test
    start_date: str
        Backtest start date, in YYYY-MM-DD format. Can be either string or datetime
    name: str
        Name of the backtest (for labeling purposes)

    Returns
    -------
    bt.Backtest
        Backtest object for buy and hold strategy
    """
    prices = get_data(symbol, start_date)
    bt_strategy = bt.Strategy(
        name,
        [
            bt.algos.RunOnce(),
            bt.algos.SelectAll(),
            bt.algos.WeighEqually(),
            bt.algos.Rebalance(),
        ],
    )
    return bt.Backtest(bt_strategy, prices)


@log_start_end(log=logger)
def ema_strategy(
    symbol: str,
    data: pd.DataFrame,
    ema_length: int = 20,
    spy_bt: bool = True,
    no_bench: bool = False,
) -> bt.backtest.Result:
    """Perform backtest for simple EMA strategy.  Buys when price>EMA(l).

    Parameters
    ----------
    symbol: str
        Stock ticker
    data: pd.DataFrame
        Dataframe of prices
    ema_length: int
        Length of ema window
    spy_bt: bool
        Boolean to add spy comparison
    no_bench: bool
        Boolean to not show buy and hold comparison

    Returns
    -------
    bt.backtest.Result
        Backtest results
    """

    # TODO: Help Wanted!
    # Implement support for backtesting on intraday data
    if is_intraday(data):
        return None
    data.index = pd.to_datetime(data.index.date)

    symbol = symbol.lower()
    ema = pd.DataFrame()
    start_date = data.index[0]
    close_col = ta_helpers.check_columns(data, high=False, low=False)
    if close_col is None:
        return bt.backtest.Result()
    prices = pd.DataFrame(data[close_col])
    prices.columns = [symbol]
    ema[symbol] = ta.ema(prices[symbol], ema_length)
    bt_strategy = bt.Strategy(
        "AboveEMA",
        [
            bt.algos.SelectWhere(prices >= ema),
            bt.algos.WeighEqually(),
            bt.algos.Rebalance(),
        ],
    )
    bt_backtest = bt.Backtest(bt_strategy, prices)
    backtests = [bt_backtest]
    if spy_bt:
        spy_bt = buy_and_hold("spy", start_date, "SPY Hold")
        backtests.append(spy_bt)
    if not no_bench:
        stock_bt = buy_and_hold(symbol, start_date, symbol.upper() + " Hold")
        backtests.append(stock_bt)

    res = bt.run(*backtests)
    return res


@log_start_end(log=logger)
def emacross_strategy(
    symbol: str,
    data: pd.DataFrame,
    short_length: int = 20,
    long_length: int = 50,
    spy_bt: bool = True,
    no_bench: bool = False,
    shortable: bool = True,
) -> bt.backtest.Result:
    """Perform backtest for simple EMA strategy. Buys when price>EMA(l).

    Parameters
    ----------
    symbol : str
        Stock ticker
    data : pd.DataFrame
        Dataframe of prices
    short_length : int
        Length of short ema window
    long_length : int
        Length of long ema window
    spy_bt : bool
        Boolean to add spy comparison
    no_bench : bool
        Boolean to not show buy and hold comparison
    shortable : bool
        Boolean to allow for selling of the stock at cross

    Returns
    -------
    Result
        Backtest results
    """
    symbol = symbol.lower()
    start_date = data.index[0]
    close_col = ta_helpers.check_columns(data, low=False, high=False)
    if close_col is None:
        return bt.backtest.Result()
    prices = pd.DataFrame(data[close_col])
    prices.columns = [symbol]
    short_ema = pd.DataFrame(ta.ema(prices[symbol], short_length))
    short_ema.columns = [symbol]
    long_ema = pd.DataFrame(ta.ema(prices[symbol], long_length))
    long_ema.columns = [symbol]

    # signals
    signals = long_ema.copy()
    signals[short_ema > long_ema] = 1.0
    signals[short_ema <= long_ema] = -1.0 * shortable
    signals[long_ema.isnull()] = 0.0

    combined_data = bt.merge(signals, prices, short_ema, long_ema)
    combined_data.columns = ["signal", "price", "ema_short", "ema_long"]
    bt_strategy = bt.Strategy(
        "EMACross",
        [
            bt.algos.WeighTarget(signals),
            bt.algos.Rebalance(),
        ],
    )
    bt_backtest = bt.Backtest(bt_strategy, prices)
    backtests = [bt_backtest]
    if spy_bt:
        spy_bt = buy_and_hold("spy", start_date, "SPY Hold")
        backtests.append(spy_bt)
    if not no_bench:
        stock_bt = buy_and_hold(symbol, start_date, symbol.upper() + " Hold")
        backtests.append(stock_bt)

    with warnings.catch_warnings():
        warnings.filterwarnings("ignore")
        res = bt.run(*backtests)
    return res


@log_start_end(log=logger)
def rsi_strategy(
    symbol: str,
    data: pd.DataFrame,
    periods: int = 14,
    low_rsi: int = 30,
    high_rsi: int = 70,
    spy_bt: bool = True,
    no_bench: bool = False,
    shortable: bool = True,
) -> bt.backtest.Result:
    """Perform backtest for simple EMA strategy. Buys when price>EMA(l).

    Parameters
    ----------
    symbol : str
        Stock ticker
    data : pd.DataFrame
        Dataframe of prices
    periods : int
        Number of periods for RSI calculation
    low_rsi : int
        Low RSI value to buy
    high_rsi : int
        High RSI value to sell
    spy_bt : bool
        Boolean to add spy comparison
    no_bench : bool
        Boolean to not show buy and hold comparison
    shortable : bool
        Flag to disable the ability to short sell

    Returns
    -------
    Result
        Backtest results
    """
    symbol = symbol.lower()
    start_date = data.index[0]
    close_col = ta_helpers.check_columns(data, high=False, low=False)
    if close_col is None:
        return pd.DataFrame()
    prices = pd.DataFrame(data[close_col])
    prices.columns = [symbol]
    rsi = pd.DataFrame(ta.rsi(prices[symbol], periods))
    rsi.columns = [symbol]

    signal = 0 * rsi.copy()
    signal[rsi > high_rsi] = -1 * shortable
    signal[rsi < low_rsi] = 1
    signal[rsi.isnull()] = 0

    merged_data = bt.merge(signal, prices)
    merged_data.columns = ["signal", "price"]

    warnings.simplefilter(action="ignore", category=FutureWarning)
    bt_strategy = bt.Strategy(
        "RSI Reversion", [bt.algos.WeighTarget(signal), bt.algos.Rebalance()]
    )
    bt_backtest = bt.Backtest(bt_strategy, prices)
    bt_backtest = bt.Backtest(bt_strategy, prices)
    backtests = [bt_backtest]
    # Once the bt package replaces pd iteritems with items we can remove this
    with warnings.catch_warnings():
        if spy_bt:
            spy_bt = buy_and_hold("spy", start_date, "SPY Hold")
            backtests.append(spy_bt)
        if not no_bench:
            stock_bt = buy_and_hold(symbol, start_date, symbol.upper() + " Hold")
            backtests.append(stock_bt)

    with warnings.catch_warnings():
        warnings.filterwarnings("ignore")
        res = bt.run(*backtests)
    return res
