"""Main module of the package"""
# Standard library imports
import logging
from typing import Optional, Any, Union
import datetime
import time

# Third party imports
import pandas as pd
from tqdm.auto import tqdm

# Local imports

# Global constants
LOGGER = logging.getLogger(__name__)


def run_backtest(
        df_positions : pd.DataFrame,
        df_real_exec_prices : pd.DataFrame,
        df_perfect_exec_prices : pd.DataFrame = None,
        is_to_neutralize : bool=True,
        const_trading_fees_percent : float=0.01,
) -> pd.DataFrame:
    """Backtest positions to understand if they can generate PNL

    Args:
        df_positions (pd.DataFrame): Positions we want to take
        df_execution_prices_full (pd.DataFrame): Prices of assets in higher resolution
        is_to_neutralize (bool): Flag if to have long-short equal positions
        const_trading_fees_percent (float): Broker commission fee in percent

    Returns:
        pd.DataFrame: columns with different PNLs generated at every tick
    """
    LOGGER.debug("Run backtest")
    # print(df_positions.T.tail(3).T.tail(3).to_html())
    df_backtest_res = pd.DataFrame()
    # Neutralize
    if is_to_neutralize:
        LOGGER.debug("---> Neutralize positions for every datetime")
        df_positions = _neutralize(df_positions)
    # Scale to 1.0
    LOGGER.debug("---> Scale positions to 1 for every datetime")
    df_positions = _scale(df_positions)
    # Calculate holding pnl
    # which is a pnl generated by positions taken in the past
    LOGGER.debug("---> Calculate holding PNL")
    df_backtest_res["PNL before_costs"] = _calc_ser_holding_pnl(
        df_positions, df_real_exec_prices)
    # DataFrame how we would like to change our current positions
    LOGGER.debug("---> Calculate the wanted position change on every tick")
    df_pos_change_wanted = df_positions - df_positions.shift(1)
    if df_perfect_exec_prices is not None:
        df_lost_value = (
            (df_real_exec_prices - df_perfect_exec_prices) /
            df_perfect_exec_prices
        )
        df_execution_fee = df_pos_change_wanted * df_lost_value
        df_backtest_res["PNL execution_fee"] = df_execution_fee.sum(axis=1)
    # Volume traded at the current tick
    df_backtest_res["trading_volume"] = \
        df_pos_change_wanted.abs().sum(axis=1)
    # Brokers trading commission
    df_backtest_res["PNL const_trading_fee"] = (
        df_backtest_res["trading_volume"] *
        const_trading_fees_percent / 100.0
    )
    # Add column with with final PNL results
    df_backtest_res["PNL after_costs"] = (
        df_backtest_res["PNL before_costs"] -
        # df_backtest_res["PNL execution_fee"] -
        df_backtest_res["PNL const_trading_fee"]
    )
    df_backtest_res["PNL half_costs"] = (
        df_backtest_res["PNL before_costs"] +
        df_backtest_res["PNL after_costs"]
    ) / 2.0
    # Add more columns with info
    df_backtest_res["booksize"] = df_positions.abs().sum(axis=1)
    df_backtest_res["max weight"] = df_positions.abs().max(axis=1)
    df_backtest_res["long count"] = \
        df_positions[df_positions > 0].count(axis=1)
    df_backtest_res["short count"] = \
        df_positions[df_positions < 0].count(axis=1)
    df_backtest_res["long value"] = \
        df_positions[df_positions > 0].sum(axis=1).abs()
    df_backtest_res["short value"] = \
        df_positions[df_positions < 0].sum(axis=1).abs()
    return change_final_columns_order(df_backtest_res)


def change_final_columns_order(df_backtest_res):
    """"""
    columns_ordered = []
    columns_pnl = []
    for column in df_backtest_res.columns:
        if "PNL" in column:
            columns_pnl.append(column)
            continue
        columns_ordered.append(column)
    df_backtest_res = df_backtest_res[columns_pnl + columns_ordered]
    return df_backtest_res


def _calc_ser_holding_pnl(
        df_positions : pd.DataFrame,
        df_execution_prices : pd.DataFrame
) -> pd.Series:
    """Calculate PNL generated by holding positions taken at last tick"""
    LOGGER.debug("------> Shift positions on 1 tick backward")

    df_previous_pos = df_positions.shift(1)
    df_prices_change_pct = \
        (df_execution_prices / df_execution_prices.shift(1)) - 1.0
    LOGGER.debug(
        "------> Multiply previous tick positions on price change percent")
    df_holding_pnl = df_previous_pos.multiply(df_prices_change_pct)
    return df_holding_pnl.sum(axis=1)


def _neutralize(df_positions : pd.DataFrame) -> pd.DataFrame:
    """Neutralize positions to be long-short equal"""
    return df_positions.sub(df_positions.mean(axis=1), axis=0)


def _scale(df_positions : pd.DataFrame) -> pd.DataFrame:
    """
    Scale to have sum of absolute positions at every tick equals to 1.0
    """
    return df_positions.div(df_positions.abs().sum(axis=1), axis=0)
