import copy
import csv
import json
import numbers
import os
import time

import numpy as np
import pandas as pd
from catalyst.assets._assets import TradingPair
from catalyst.exchange.utils.exchange_utils import get_algo_folder
from catalyst.utils.paths import data_root, ensure_directory
from operator import itemgetter

s3_conn = []
mailgun = []


def trend_direction(series):
    if series[-1] is np.nan or series[-1] is np.nan:
        return None

    if series[-1] > series[-2]:
        return 'up'
    else:
        return 'down'


def crossover(source, target):
    """
    The `x`-series is defined as having crossed over `y`-series if the value
    of `x` is greater than the value of `y` and the value of `x` was less than
    the value of `y` on the bar immediately preceding the current bar.

    Parameters
    ----------
    source: Series
    target: Series

    Returns
    -------
    bool

    """
    if isinstance(target, numbers.Number):
        if source[-1] is np.nan or source[-2] is np.nan \
           or target is np.nan:
            return False

        if source[-1] >= target > source[-2]:
            return True
        else:
            return False

    else:
        if source[-1] is np.nan or source[-2] is np.nan \
           or target[-1] is np.nan or target[-2] is np.nan:
            return False

        if source[-1] > target[-1] and source[-2] < target[-2]:
            return True
        else:
            return False


def crossunder(source, target):
    """
    The `x`-series is defined as having crossed under `y`-series if the value
    of `x` is less than the value of `y` and the value of `x` was greater than
    the value of `y` on the bar immediately preceding the current bar.

    Parameters
    ----------
    source: Series
    target: Series

    Returns
    -------
    bool

    """
    if isinstance(target, numbers.Number):
        if source[-1] is np.nan or source[-2] is np.nan \
           or target is np.nan:
            return False

        if source[-1] < target <= source[-2]:
            return True
        else:
            return False
    else:
        if source[-1] is np.nan or source[-2] is np.nan \
           or target[-1] is np.nan or target[-2] is np.nan:
            return False

        if source[-1] < target[-1] and source[-2] >= target[-2]:
            return True
        else:
            return False


def vwap(df):
    """
    Volume-weighted average price (VWAP) is a ratio generally used by
    institutional investors and mutual funds to make buys and sells so as not
    to disturb the market prices with large orders. It is the average share
    price of a stock weighted against its trading volume within a particular
    time frame, generally one day.

    Read more: Volume Weighted Average Price - VWAP
    https://www.investopedia.com/terms/v/vwap.asp#ixzz4xt922daE

    Parameters
    ----------
    df: pd.DataFrame

    Returns
    -------

    """
    if 'close' not in df.columns or 'volume' not in df.columns:
        raise ValueError('price data must include `volume` and `close`')

    vol_sum = np.nansum(df['volume'].values)

    try:
        ret = np.nansum(df['close'].values * df['volume'].values) / vol_sum
    except ZeroDivisionError:
        ret = np.nan

    return ret


def set_position_row(row, asset, asset_values=list()):
    """
    Apply the position data as individual columns.

    Parameters
    ----------
    row: dict[str, Object]
    asset: TradingPair
    asset_values: list[str]
        If a recorded_col contains a tuple which first value is an asset
        matching a position, its value will be displayed with the
        position and not in the index.

    Returns
    -------

    """
    asset_cols = ['symbol']
    row['symbol'] = asset.symbol

    position = next((p for p in row['positions'] if p['sid'] == asset), None)

    columns = ['amount', 'cost_basis', 'last_sale_price']
    for column in columns:
        if position is not None:
            row[column] = position[column]

        else:
            row[column] = 0

        asset_cols.append(column)

    values = asset_values[asset] if asset in asset_values else list()
    for column in values:
        row[column] = values[column]

        asset_cols.append(column)

    return asset_cols


def prepare_stats(stats, recorded_cols=list()):
    """
    Prepare the stats DataFrame for user-friendly output.

    Parameters
    ----------
    stats: list[Object]
    recorded_cols: list[str]

    Returns
    -------

    """
    asset_cols = list()

    stats = copy.deepcopy(stats)
    # Using a copy since we are adding rows inside the loop.
    for row_index, row_data in enumerate(list(stats)):
        assets = [p['sid'] for p in row_data['positions']]

        asset_values = dict()
        if recorded_cols is not None:
            for column in recorded_cols[:]:
                value = row_data[column]
                if isinstance(value, pd.Series):
                    value = value.to_dict()

                if type(value) is dict:
                    for asset in value:
                        if not isinstance(asset, TradingPair):
                            break

                        if asset not in assets:
                            assets.append(asset)

                        if asset not in asset_values:
                            asset_values[asset] = dict()

                        asset_values[asset][column] = value[asset]

        if len(assets) == 1:
            row = stats[row_index]
            asset_cols = set_position_row(row, assets[0], asset_values)

        elif len(assets) > 1:
            for asset_index, asset in enumerate(assets):
                if asset_index > 0:
                    row = copy.deepcopy(row_data)
                    stats.append(row)

                else:
                    row = stats[row_index]

                asset_cols = set_position_row(row, assets[asset_index],
                                              asset_values)

    df = pd.DataFrame(stats)
    df['orders'] = df['orders'].apply(lambda orders: len(orders))
    df['transactions'] = df['transactions'].apply(
        lambda transactions: len(transactions)
    )
    index_cols = [
        'period_close', 'starting_cash', 'ending_cash', 'portfolio_value',
        'pnl', 'long_exposure', 'short_exposure', 'orders', 'transactions',
    ]

    # Removing the asset specific entries
    if recorded_cols is not None:
        recorded_cols = [x for x in recorded_cols if x not in asset_cols]
        for column in recorded_cols:
            index_cols.append(column)

    if asset_cols:
        columns = asset_cols
        df.set_index(index_cols, drop=True, inplace=True)

    else:
        columns = index_cols
        columns.remove('period_close')
        df.set_index('period_close', drop=False, inplace=True)

    df.dropna(axis=1, how='all', inplace=True)
    df.sort_index(axis=0, level=0, inplace=True)

    return df, columns


def set_print_settings():
    pd.set_option('display.expand_frame_repr', False)
    pd.set_option('precision', 8)
    pd.set_option('display.width', 1000)
    pd.set_option('display.max_colwidth', 1000)


def get_pretty_stats(stats, recorded_cols=None, num_rows=10, show_tail=True):
    """
    Format and print the last few rows of a statistics DataFrame.
    See the pyfolio project for the data structure.

    Parameters
    ----------
    stats: list[Object]
        An array of statistics for the period.

    num_rows: int
        The number of rows to display on the screen.

    Returns
    -------
    str

    """
    if isinstance(stats, pd.DataFrame):
        stats = list(stats.T.to_dict().values())
        stats.sort(key=itemgetter('period_close'))

    if len(stats) > num_rows:
        display_stats = stats[-num_rows:] if show_tail else stats[0:num_rows]
    else:
        display_stats = stats

    df, columns = prepare_stats(
        display_stats, recorded_cols=recorded_cols
    )
    set_print_settings()
    return df.to_string(columns=columns)


def get_csv_stats(stats, recorded_cols=None):
    """
    Create a CSV buffer from the stats DataFrame.

    Parameters
    ----------
    path: str
    stats: list[Object]
    recorded_cols: list[str]

    Returns
    -------

    """
    df, columns = prepare_stats(stats, recorded_cols=recorded_cols)

    return df.to_csv(
        None,
        columns=columns,
        # encoding='utf-8',
        quoting=csv.QUOTE_NONNUMERIC
    ).encode()


def stats_to_s3(uri, stats, algo_namespace, recorded_cols=None,
                folder='catalyst/stats', bytes_to_write=None):
    """
    Uploads the performance stats to a S3 bucket.

    Parameters
    ----------
    uri: str
    stats: list[Object]
    algo_namespace: str
    recorded_cols: list[str]
    folder: str
    bytes_to_write: str
        Option to reuse bytes instead of re-computing the csv

    Returns
    -------

    """
    if not s3_conn:
        import boto3
        s3_conn.append(boto3.resource('s3'))

    s3 = s3_conn[0]

    if bytes_to_write is None:
        bytes_to_write = get_csv_stats(stats, recorded_cols=recorded_cols)

    now = pd.Timestamp.utcnow()
    timestr = now.strftime('%Y%m%d')
    pid = os.getpid()

    parts = uri.split('//')
    path = '{folder}/{algo}/{time}-{algo}-{pid}.csv'.format(
        folder=folder,
        algo=algo_namespace,
        time=timestr,
        pid=pid,
    )
    obj = s3.Object(parts[1], path)
    obj.put(Body=bytes_to_write)


def email_error(algo_name, dt, e, environ=None):
    import requests
    import traceback

    if not mailgun:
        root = data_root(environ)
        filename = os.path.join(root, 'mailgun.json')
        if not os.path.exists(filename):
            raise ValueError(
                'mailgun.json not found in the catalyst data folder'
            )

        with open(filename) as data_file:
            mailgun.append(json.load(data_file))

    mg = mailgun[0]

    return requests.post(
        mg['url'],
        auth=("api", mg['api']),
        data={
            "from": mg['from'],
            "to": mg['to'],
            "subject": 'Error: {}'.format(algo_name),
            "text": '{}\n\n{}\n{}'.format(
                dt, e, traceback.format_exc()
            )})


def stats_to_algo_folder(stats, algo_namespace,
                         folder_name, recorded_cols=None):
    """
    Saves the performance stats to the algo local folder.

    Parameters
    ----------
    stats: list[Object]
    algo_namespace: str
    folder_name: str
    recorded_cols: list[str]

    Returns
    -------
    str

    """
    bytes_to_write = get_csv_stats(stats, recorded_cols=recorded_cols)

    timestr = time.strftime('%Y%m%d')
    folder = get_algo_folder(algo_namespace)

    stats_folder = os.path.join(folder, folder_name)
    ensure_directory(stats_folder)

    filename = os.path.join(stats_folder, '{}.csv'.format(timestr))

    with open(filename, 'wb') as handle:
        handle.write(bytes_to_write)

    return bytes_to_write


def df_to_string(df):
    """
    Create a formatted str representation of the DataFrame.

    Parameters
    ----------
    df: DataFrame

    Returns
    -------
    str

    """
    pd.set_option('display.expand_frame_repr', False)
    pd.set_option('precision', 8)
    pd.set_option('display.width', 1000)
    pd.set_option('display.max_colwidth', 1000)

    return df.to_string()


def extract_orders(perf):
    order_list = perf.orders.values
    all_orders = [t for sublist in order_list for t in sublist]
    all_orders.sort(key=lambda o: o['dt'])

    orders = pd.DataFrame(all_orders)
    if not orders.empty:
        orders.set_index('dt', inplace=True, drop=True)
    return orders


def extract_transactions(perf):
    """
    Compute indexes for buy and sell transactions

    Parameters
    ----------
    perf: DataFrame
        The algo performance DataFrame.

    Returns
    -------
    DataFrame
        A DataFrame of transactions.

    """
    trans_list = perf.transactions.values
    all_trans = [t for sublist in trans_list for t in sublist]
    all_trans.sort(key=lambda t: t['dt'])

    transactions = pd.DataFrame(all_trans)
    if not transactions.empty:
        transactions.set_index('dt', inplace=True, drop=True)
    return transactions
