from copy import copy
from collections import namedtuple

import datetime
import numpy as np
import pandas as pd

from syscore.genutils import transfer_object_attributes
from syscore.pdutils import make_df_from_list_of_named_tuple
from syscore.objects import header, table, body_text, arg_not_supplied, missing_data

from sysdata.data_blob import dataBlob
from sysproduction.data.orders import dataOrders
from sysproduction.data.instruments import diagInstruments

from sysproduction.reporting.risk_report import  get_current_annualised_stdev_for_instrument

def trades_info(
    data=arg_not_supplied,
    calendar_days_back=1,
    end_date=arg_not_supplied,
    start_date=arg_not_supplied,
):
    """
    Report on system status

    :param: data blob
    :return: list of formatted output items
    """
    if data is arg_not_supplied:
        data = dataBlob()

    if end_date is arg_not_supplied:
        end_date = datetime.datetime.now()

    if start_date is arg_not_supplied:
        start_date = end_date - datetime.timedelta(days=calendar_days_back)

    results_object = get_trades_report_data(
        data, start_date=start_date, end_date=end_date
    )
    formatted_output = format_trades_data(results_object)

    return formatted_output


def get_trades_report_data(data, start_date, end_date):

    broker_orders = get_recent_broker_orders(data, start_date, end_date)
    if len(broker_orders) == 0:
        empty_df = pd.DataFrame()
        results_object = dict(overview=empty_df)
        return results_object

    overview = broker_orders[
        [
            "instrument_code",
            "strategy_name",
            "contract_date",
            "fill_datetime",
            "fill",
            "filled_price",
        ]
    ]

    delays = create_delay_df(broker_orders)
    raw_slippage = create_raw_slippage_df(broker_orders)
    vol_slippage = create_vol_norm_slippage_df(raw_slippage, data)
    cash_slippage = create_cash_slippage_df(raw_slippage, data)

    summary_dict = {}
    item_list = [
        "delay",
        "bid_ask",
        "execution",
        "versus_limit",
        "versus_parent_limit",
        "total_trading",
    ]
    detailed_raw_results = get_stats_for_slippage_groups(
        raw_slippage, item_list)
    summary_dict.update(detailed_raw_results)

    item_list = [
        "delay_vol",
        "bid_ask_vol",
        "execution_vol",
        "versus_limit_vol",
        "versus_parent_limit_vol",
        "total_trading_vol",
    ]
    detailed_vol_results = get_stats_for_slippage_groups(
        vol_slippage, item_list)
    summary_dict.update(detailed_vol_results)

    item_list = [
        "delay_cash",
        "bid_ask_cash",
        "execution_cash",
        "versus_limit_cash",
        "versus_parent_limit_cash",
        "total_trading_cash",
    ]
    detailed_cash_results = get_stats_for_slippage_groups(
        cash_slippage, item_list)
    summary_dict.update(detailed_cash_results)

    results_object = dict(
        overview=overview,
        delays=delays,
        raw_slippage=raw_slippage,
        vol_slippage=vol_slippage,
        cash_slippage=cash_slippage,
        summary_dict=summary_dict,
    )

    return results_object


def format_trades_data(results_object):
    """
    Put the results into a printable format

    :param results_dict: dict, keys are different segments
    :return:
    """

    formatted_output = []

    formatted_output.append(
        header("Trades report produced on %s" % (str(datetime.datetime.now())))
    )

    if len(results_object["overview"]) == 0:
        formatted_output.append(body_text("No trades in relevant period"))

        return formatted_output

    table1_df = results_object["overview"]
    table1 = table("Broker orders", table1_df)
    formatted_output.append(table1)

    table2_df = results_object["delays"]
    table2 = table("Delays", table2_df)
    formatted_output.append(table2)

    table3_df = results_object["raw_slippage"]
    table3 = table("Slippage (ticks per lot)", table3_df)
    formatted_output.append(table3)

    table4_df = results_object["vol_slippage"]
    table4 = table(
        "Slippage (normalised by annual vol, BP of annual SR)",
        table4_df)
    formatted_output.append(table4)

    table5_df = results_object["cash_slippage"]
    table5 = table("Slippage (In base currency)", table5_df)
    formatted_output.append(table5)

    summary_results_dict = results_object["summary_dict"]
    for summary_table_name, summary_table_item in summary_results_dict.items():
        summary_table = table(
            "Summary %s" %
            summary_table_name,
            summary_table_item)
        formatted_output.append(summary_table)

    return formatted_output


tradesData = namedtuple(
    "tradesData",
    [
        "order_id",
        "instrument_code",
        "strategy_name",
        "contract_date",
        "fill",
        "filled_price",
        "mid_price",
        "side_price",
        "offside_price",
        "parent_reference_price",  # from contract order
        "parent_reference_datetime",  # from instrument order
        "submit_datetime",
        "fill_datetime",
        "limit_price",
        "trade",
        "buy_or_sell",
        "parent_limit_price",
        "commission",
    ],
)

data = dataBlob()


def get_recent_broker_orders(data, start_date, end_date):
    data_orders = dataOrders(data)
    order_id_list = data_orders.get_historic_broker_order_ids_in_date_range(
        start_date, end_date
    )
    orders_as_list = [get_tuple_object_from_order_id(
        data, order_id) for order_id in order_id_list]
    pdf = make_df_from_list_of_named_tuple(tradesData, orders_as_list)

    return pdf


def get_tuple_object_from_order_id(data, order_id):
    data_orders = dataOrders(data)
    order = data_orders.get_historic_broker_order_from_order_id_with_execution_data(
        order_id)
    tuple_object = transfer_object_attributes(tradesData, order)

    return tuple_object


def create_delay_df(broker_orders):
    delay_data_as_list = [
        delay_row(
            broker_orders.iloc[irow]) for irow in range(
            len(broker_orders))]
    delay_data_df = pd.concat(delay_data_as_list, axis=1)
    delay_data_df = delay_data_df.transpose()
    delay_data_df.index = broker_orders.index

    return delay_data_df


def delay_row(order_row):
    submit_minus_generated, filled_minus_submit = delay_calculations_for_order_row(
        order_row)
    new_order_row = copy(order_row)
    new_order_row = new_order_row[
        [
            "instrument_code",
            "strategy_name",
            "parent_reference_datetime",
            "submit_datetime",
            "fill_datetime",
        ]
    ]
    new_order_row = new_order_row.append(
        pd.Series(
            [submit_minus_generated, filled_minus_submit],
            index=["submit_minus_generated", "filled_minus_submit"],
        )
    )

    return new_order_row


def delay_calculations_for_order_row(order_row):

    submit_minus_generated = delay_calc(
        order_row.parent_reference_datetime, order_row.submit_datetime
    )

    filled_minus_submit = delay_calc(
        order_row.submit_datetime,
        order_row.fill_datetime)

    return submit_minus_generated, filled_minus_submit


def delay_calc(first_time, second_time):
    if first_time is None or second_time is None:
        return np.nan

    time_diff = second_time - first_time
    time_diff_seconds = time_diff.total_seconds()

    if time_diff_seconds < 0:
        return np.nan

    return time_diff_seconds


def create_raw_slippage_df(broker_orders):
    raw_slippage_data_as_list = [
        raw_slippage_row(
            broker_orders.iloc[irow]) for irow in range(
            len(broker_orders))]
    raw_slippage_df = pd.concat(raw_slippage_data_as_list, axis=1)
    raw_slippage_df = raw_slippage_df.transpose()
    raw_slippage_df.index = broker_orders.index

    return raw_slippage_df


def raw_slippage_row(order_row):
    (
        delay,
        bid_ask,
        execution,
        versus_limit,
        versus_parent_limit,
        total_trading,
    ) = price_calculations_for_order_row(order_row)
    new_order_row = copy(order_row)
    new_order_row = new_order_row[
        [
            "instrument_code",
            "strategy_name",
            "trade",
            "parent_reference_price",
            "parent_limit_price",
            "mid_price",
            "side_price",
            "offside_price",
            "limit_price",
            "filled_price",
        ]
    ]
    new_order_row = new_order_row.append(
        pd.Series(
            [
                delay,
                bid_ask,
                execution,
                versus_limit,
                versus_parent_limit,
                total_trading,
            ],
            index=[
                "delay",
                "bid_ask",
                "execution",
                "versus_limit",
                "versus_parent_limit",
                "total_trading",
            ],
        )
    )

    return new_order_row


def price_calculations_for_order_row(order_row):
    buying_multiplier = order_row.buy_or_sell

    # Following are always floats: parent_reference_price, limit_price,
    # calculated_mid_price, calculated_side_price, fill_price
    delay = price_slippage(
        buying_multiplier,
        order_row.parent_reference_price,
        order_row.mid_price,
    )

    bid_ask = price_slippage(
        buying_multiplier,
        order_row.mid_price,
        order_row.side_price,
    )

    execution = price_slippage(
        buying_multiplier,
        order_row.side_price,
        order_row.filled_price,
    )

    total_trading = bid_ask + execution

    versus_limit = price_slippage(
        buying_multiplier,
        order_row.limit_price,
        order_row.filled_price)

    versus_parent_limit = price_slippage(
        buying_multiplier,
        order_row.parent_limit_price,
        order_row.filled_price,
    )

    return delay, bid_ask, execution, versus_limit, versus_parent_limit, total_trading


def price_slippage(buying_multiplier, first_price, second_price):
    # Slippage is always negative (bad) positive (good)
    # This will return a negative number if second price is adverse versus
    # first price
    if first_price is None or second_price is None:
        return np.nan

    # 1 if buying, -1 if selling
    # if buying, want second price to be lower than first
    # if selling, want second price to be higher than first
    slippage = buying_multiplier * (first_price - second_price)
    return slippage


def create_cash_slippage_df(raw_slippage, data):
    # What does this slippage mean in money terms

    cash_slippage_data_as_list = [
        cash_slippage_row(raw_slippage.iloc[irow], data)
        for irow in range(len(raw_slippage))
    ]
    cash_slippage_df = pd.concat(cash_slippage_data_as_list, axis=1)
    cash_slippage_df = cash_slippage_df.transpose()
    cash_slippage_df.index = raw_slippage.index

    return cash_slippage_df


def cash_slippage_row(slippage_row, data):
    # rewrite
    (
        delay_cash,
        bid_ask_cash,
        execution_cash,
        versus_limit_cash,
        versus_parent_limit_cash,
        total_trading_cash,
        value_of_price_point,
    ) = cash_calculations_for_slippage_row(slippage_row, data)
    new_slippage_row = copy(slippage_row)
    new_slippage_row = new_slippage_row[
        [
            "instrument_code",
            "strategy_name",
            "trade",
        ]
    ]
    new_slippage_row = new_slippage_row.append(
        pd.Series(
            [
                value_of_price_point,
                delay_cash,
                bid_ask_cash,
                execution_cash,
                versus_limit_cash,
                versus_parent_limit_cash,
                total_trading_cash,
            ],
            index=[
                "value_of_price_point",
                "delay_cash",
                "bid_ask_cash",
                "execution_cash",
                "versus_limit_cash",
                "versus_parent_limit_cash",
                "total_trading_cash",
            ],
        )
    )

    return new_slippage_row


def cash_calculations_for_slippage_row(slippage_row, data):
    # What's a tick worth in base currency?
    diag_instruments = diagInstruments(data)
    value_of_price_point = diag_instruments.get_point_size_base_currency(
        slippage_row.instrument_code
    )
    input_items = [
        "delay",
        "bid_ask",
        "execution",
        "versus_limit",
        "versus_parent_limit",
        "total_trading",
    ]
    output = [value_of_price_point * slippage_row[input_name]
              for input_name in input_items]

    return tuple(output + [value_of_price_point])


def create_vol_norm_slippage_df(raw_slippage, data):
    # What does this slippage mean in vol normalised terms
    for irow in range(len(raw_slippage)):
        vol_slippage_row(raw_slippage.iloc[irow], data)

    vol_slippage_data_as_list = [
        vol_slippage_row(raw_slippage.iloc[irow], data)
        for irow in range(len(raw_slippage))
    ]
    vol_slippage_df = pd.concat(vol_slippage_data_as_list, axis=1)
    vol_slippage_df = vol_slippage_df.transpose()
    vol_slippage_df.index = raw_slippage.index

    return vol_slippage_df


def vol_slippage_row(slippage_row, data):
    # rewrite
    (
        vol_delay,
        vol_bid_ask,
        vol_execution,
        vol_versus_limit,
        vol_versus_parent_limit,
        total_trading_vol,
        last_annual_vol,
    ) = vol_calculations_for_slippage_row(slippage_row, data)
    new_slippage_row = copy(slippage_row)
    new_slippage_row = new_slippage_row[
        [
            "instrument_code",
            "strategy_name",
            "trade",
        ]
    ]
    new_slippage_row = new_slippage_row.append(
        pd.Series(
            [
                last_annual_vol,
                vol_delay,
                vol_bid_ask,
                vol_execution,
                vol_versus_limit,
                vol_versus_parent_limit,
                total_trading_vol,
            ],
            index=[
                "last_annual_vol",
                "delay_vol",
                "bid_ask_vol",
                "execution_vol",
                "versus_limit_vol",
                "versus_parent_limit_vol",
                "total_trading_vol",
            ],
        )
    )

    return new_slippage_row


def vol_calculations_for_slippage_row(slippage_row, data):

    last_annual_vol = get_last_annual_vol_for_slippage_row(slippage_row, data)

    input_items = [
        "delay",
        "bid_ask",
        "execution",
        "versus_limit",
        "versus_parent_limit",
        "total_trading",
    ]
    output = [10000 * slippage_row[input_name] /
              last_annual_vol for input_name in input_items]

    return tuple(output + [last_annual_vol])


def get_last_annual_vol_for_slippage_row(slippage_row, data):
    instrument_code = slippage_row.instrument_code
    last_annual_vol = get_current_annualised_stdev_for_instrument(data,
        instrument_code)

    return last_annual_vol


def get_stats_for_slippage_groups(df_to_process, item_list):
    results = {}
    for item_name in item_list:

        sum_data = df_to_process.groupby(
            ["strategy_name", "instrument_code"]).agg({item_name: "sum"})
        count_data = df_to_process.groupby(
            ["strategy_name", "instrument_code"]).agg({item_name: "count"})
        avg_data = sum_data / count_data

        try:
            std = df_to_process.groupby(
                ["strategy_name", "instrument_code"]).agg({item_name: "std"})
        except pd.core.base.DataError:
            # not enough items to calculate standard deviation
            std = np.nan

        lower_range = avg_data + (-2 * std)
        upper_range = avg_data + (2 * std)

        results[item_name + " Sum"] = sum_data
        results[item_name + " Count"] = count_data
        results[item_name + " Mean"] = avg_data
        results[item_name + " Lower range"] = lower_range
        results[item_name + " Upper range"] = upper_range

        total_sum_data = df_to_process.groupby(["strategy_name"]).agg(
            {item_name: "sum"}
        )

        results[item_name + " Total Sum"] = total_sum_data

    return results
