"""Tools for use in DeFi
"""
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import datetime, requests
from matplotlib.gridspec import GridSpec
from pandas.plotting import register_matplotlib_converters

register_matplotlib_converters()


def iloss(price_ratio, numerical=False):
    """return the impermanent loss result in compare with buy&hold A&B assets


    Args:
        price_ratio (float): Variation A Asset / Variation B Asset

            price_ratio formula:
                price_ratio = (var_A/100 + 1) / (var_B/100 + 1)
                    var_A: Asset A % variation
                    var_B: Asset B % variation

        numerical (bool): if True, returns impermanent loss as a decimal expr, ie "5%"" => 0.05 (Default: False)

    Returns:
        TYPE: impermanent loss as a string percentual value
    """
    il = 2 * (price_ratio ** 0.5 / (1 + price_ratio)) - 1
    r = f"{il:.2%}" if not numerical else il

    return r


def compare(days, var_A=0, var_B=0, rw_pool_A=0, rw_pool_B=0, rw_pool_AB=0, fees_AB=0):
    """Compare for 2 assets, buy&hold strategy with separate staking and farming by liquidity pool providing.
        Considering: impermanent loss, fees earned and farming/staking rewards

    Args:
        days (int): days for strategy
        var_A (float, optional): Percentual variation for A token. Ex 10 for 10%
        var_B (float, optional): Percentual variation for B token. Ex 10 for 10%
        rw_pool_A (float, optional): Percentual rewards per day for one asset pool (Token A)
        rw_pool_B (float, optional): Percentual rewards per day for one asset pool (Token B)
        rw_pool_AB (float, optional): Percentual rewards per day for two asset farm (LP Token AB)
        fees_AB (float, optional): Percentual provider liquidity fees earned per day

    Returns:
        dict: Percentual returns for each strategy:
            buy_hold two assets in your wallet
            stake two assets at individual pools
            farming by liquidity pool
    """
    buy_hold = (0.5 * var_A + 0.5 * var_B) / 100
    x = (var_A / 100 + 1) / (var_B / 100 + 1)
    perdida_impermanente = 2 * (x ** 0.5 / (1 + x)) - 1

    stake = buy_hold + 0.5 * days * (rw_pool_A / 100 + rw_pool_B / 100)
    farm = buy_hold * (1 + perdida_impermanente) + days * (rw_pool_AB / 100 + fees_AB / 100)
    mejor = 'Farm' if farm > stake else 'Stake'

    return {'buy_hold': f'{buy_hold:.2%}', 'stake': f'{stake:.2%}', 'farm': f'{farm:.2%}', 'Best': mejor}


######################################################################
##                                                                  ##
##      Llama API                                                   ##
##      Public API https://docs.llama.fi/api                        ##
##                                                                  ##
######################################################################


def getProtocols():
    """Get list all DeFi components2 across all blockchains

    Returns:
        DataFrame: All DeFi dApps
    """
    url = "https://api.llama.fi/protocols"
    r = requests.get(url)
    r_json = r.json()
    df = pd.DataFrame(r_json)
    df.set_index('name', inplace=True)
    return df


def getProtocol(protocol):
    """Get metrics and historic TVL for one DeFi dApp

    Args:
        protocol (String): Name of protocol ie "Uniswap"

    Returns:
        tuple (Dictionary, DataFrame): Dictionary with protocol metadata & DataFrame with historical TVL
    """
    url = f"https://api.llama.fi/protocol/{protocol}"
    r = requests.get(url)
    r_json = r.json()

    df = pd.DataFrame(r_json['tvl'])
    df.date = pd.to_datetime(df.date, unit='s')
    df = df.set_index('date')
    del r_json['tvl']
    metadata = r_json

    return metadata, df


def getChart():
    """Get historical TVL across all DeFi dApps, cummulative result

    Returns:
        DataFrame: DataFrame date-indexed with all days TVL
    """

    url = "https://api.llama.fi/charts"
    r = requests.get(url)
    r_json = r.json()
    df = pd.DataFrame(r_json)
    df.date = pd.to_datetime(df.date, unit='s')
    df = df.set_index('date')
    return df


######################################################################
##                                                                  ##
##      CoinGecko API                                               ##
##      Public API https://www.coingecko.com/es/api                 ##
##                                                                  ##
######################################################################

def gecko_list(page=1, per_page=250):
    """Returns list of full detail conGecko currency list

    Args:
        page (int, optional): number of pages
        per_page (int, optional): number of records per page

    Returns:
        DataFrame: list of full detail conGecko currency list
    """
    url = "https://api.coingecko.com/api/v3/coins/markets"
    params = {"vs_currency": "usd", "order": "market_cap_desc", "per_page": per_page, "page": page}
    r = requests.get(url, params).json()
    df = pd.DataFrame(r)
    df.set_index('symbol', inplace=True)
    return df


def get_gecko_spot_price(tokens, quote):
    """get price of combine pairs

    Args:
        tokens (comma separated strings): ie "bitcoin,ethereum"
        quote (comma separated fiat or quote currency): ie: "usd,eur"

    Returns:
        dictionary: Returns pairs quotes
    """

    url = "https://api.coingecko.com/api/v3/simple/price"
    params = {"ids": tokens, "vs_currencies": quote}
    r = requests.get(url, params).json()
    return r


def geckoMarkets(ticker):
    """Get top100 markets (pairs, quotes, exchanges, volume, spreads and more)

    Args:
        ticker (string): gecko ID, ie "bitcoin"

    Returns:
        DataFrame: Full detail markets available
    """
    url = f"https://api.coingecko.com/api/v3/coins/{ticker}/tickers"
    r = requests.get(url).json()['tickers']
    df = pd.DataFrame(r)
    df['exchange'] = df['market'].apply(pd.Series)['name']
    df['volume_usd'] = df['converted_volume'].apply(pd.Series)['usd']
    df['price_usd'] = df['converted_last'].apply(pd.Series)['usd']

    df.set_index('exchange', inplace=True)
    cols = ['base', 'target', 'last', 'volume', 'bid_ask_spread_percentage', 'timestamp',
            'volume_usd', 'price_usd', 'trust_score']
    df = df.loc[:, cols]
    cols[4] = 'spread'
    df.columns = cols
    df.timestamp = pd.to_datetime(df.timestamp)

    return df.sort_values('volume_usd', ascending=False)


def geckoHistorical(ticker, vs_currency='usd', days='max'):
    """Historical prices from coinGecko

    Args:
        ticker (string): gecko ID, ie "bitcoin"
        vs_currency (str, optional): ie "usd" (default)
        days (str, optional): ie "20", "max" (default)

    Returns:
        DataFrame: Full history: date, price, market cap & volume
    """

    url = f"https://api.coingecko.com/api/v3/coins/{ticker}/market_chart"
    params = {"vs_currency": {vs_currency}, "days": days}
    r = requests.get(url, params).json()
    prices = pd.DataFrame(r['prices'])
    market_caps = pd.DataFrame(r['market_caps'])
    total_volumes = pd.DataFrame(r['total_volumes'])
    df = pd.concat([prices, market_caps[1], total_volumes[1]], axis=1)
    df[0] = pd.to_datetime(df[0], unit='ms')
    df.columns = ['date', 'price', 'market_caps', 'total_volumes']
    df.set_index('date', inplace=True)

    return df


def getGeckoIDs():
    """IDs List from coinGecko

    Returns:
        list: First 5000 coingecko IDs by marketCap rank
    """
    ids_list = []

    for i in range(20):
        print(f'searching coins page: {i}   ', end='\r')
        ids_list = ids_list + gecko_list(page=i, per_page=250)['id'].tolist()
    return ids_list


def farmSimulate(pair, apr, start='2021-01-01'):
    """Simulate farm result with historical prices & APR value

    Args:
        pair (list): gecko IDs list ["bitcoin",'tether']
        apr (float): ie 25 (for 25% Anual rewards)
        start (str, optional): ISO Format YYYY-MM-DD ie "2021-01-01", "2021-01-01" (default)

    Returns:
        Dict & Plot: Full farming strategy results
    """

    prices = pd.DataFrame()
    for coin in pair:
        print(f'Downloading {coin}')
        try:
            df = geckoHistorical(coin)
            prices[coin] = df['price']
        except:
            print(f'Error geting {coin} prices')

    if len(prices.columns) == 2:
        prices = prices.dropna().iloc[:]
        start = datetime.datetime.strptime(start, '%Y-%m-%d')
        farm = prices.loc[prices.index >= start]
        farm = farm.divide(farm.iloc[0])
        farm['ratio'] = farm.iloc[:, 1].divide(farm.iloc[:, 0])

        farm['iloss'] = 2 * (farm['ratio'] ** 0.5 / (1 + farm['ratio'])) - 1
        farm['rewards'] = pd.Series(2 * apr / 100 / 365, index=farm.index).cumsum()
        farm['buy_hold'] = (farm.iloc[:, 0] + farm.iloc[:, 1]) / 2
        farm['farm'] = farm.buy_hold - farm.iloss + farm.rewards

        cagrs = farm.iloc[-1] ** (1 / (365 / len(farm))) - 1
        sigmas = farm.pct_change().std() * 365 ** 0.5
        sharpes = cagrs.divide(sigmas).round(2)
        dd = farm / farm.cummax() - 1

        fig = plt.figure(figsize=(15, 8))
        gs = GridSpec(nrows=2, ncols=4, figure=fig, height_ratios=[2, 1], hspace=0.45, wspace=0.35, top=.9)
        ax_upleft = fig.add_subplot(gs[0, 0:2])
        ax_upright = fig.add_subplot(gs[0, 2:])
        cols = 4
        ax_down = [fig.add_subplot(gs[1, i]) for i in range(cols)]

        ax_upleft.plot(farm.iloss.abs(), label='Impermanent Loss')
        ax_upleft.plot(farm.rewards, label='Farming Rewards')
        ax_upleft.legend()
        ax_upleft.grid()
        ax_upleft.set_title('Impermanent Loss vs Farming Rewards')
        ax_upleft.tick_params(axis='x', rotation=45)

        ax_upright.plot(farm.iloc[:, :2])
        ax_upright.plot(farm.buy_hold)
        ax_upright.plot(farm.farm)
        ax_upright.grid()
        ax_upright.legend([pair[0], pair[1], 'Buy&Hold', 'Farming Strategy'])
        ax_upright.set_title(f'{pair[0]} vs {pair[1]} vs Buy & Hold vs Farming strategy payoff')
        ax_upright.tick_params(axis='x', rotation=45)

        cagrs[[pair[0], pair[1], 'buy_hold', 'farm']].plot(kind='bar', ax=ax_down[0])
        sigmas[[pair[0], pair[1], 'buy_hold', 'farm']].plot(kind='bar', ax=ax_down[1])
        sharpes[[pair[0], pair[1], 'buy_hold', 'farm']].plot(kind='bar', ax=ax_down[2])
        dd[[pair[0], pair[1], 'buy_hold', 'farm']].min().plot(kind='bar', ax=ax_down[3])

        ax_down[0].set_title('CAGR', fontsize=12)
        ax_down[1].set_title('Anualized Volatility', fontsize=12)
        ax_down[2].set_title('Sharpe Ratio', fontsize=12)
        ax_down[3].set_title('Max DrawDowns', fontsize=12)
        [ax_down[i].grid(alpha=0.4) for i in range(cols)]

        for i in range(cols):
            ax_down[i].spines['top'].set_visible(False)
            ax_down[i].spines['right'].set_visible(False)

        b_h = (farm.iloc[-1].iloc[0] + farm.iloc[-1].iloc[1]) / 2 - 1
        iloss = farm.iloc[-1].iloss
        rewards = farm.iloc[-1].rewards
        net_farming = b_h - iloss + rewards

        result = {'Token 1': pair[0], 'Token 2': pair[1], 'start': start.isoformat()[:10],
                  'fixed APR': f'{apr / 100:.0%}', 'Buy & Hold': f'{b_h:.2%}',
                  'Impermanent Loss': f'{iloss:.2%}', 'Farming Rewards': f'{rewards:.2%}',
                  'Farming + Rewards - IL': f'{net_farming:.2%}'}

    else:
        result = 'Error geting historical prices, see geckoIDs() function to get CoinGecko IDs'
    return result


######################################################################
##                                                                  ##
##       Pancake Swap API                                           ##
##       API https://github.com/pancakeswap/pancake-info-api        ##
##                                                                  ##
######################################################################


def toFloatPartial(df):
    for i in df.columns:
        try:
            df[[i]] = df[[i]].astype(float)
        except:
            pass
    return df


def pcsSummary(as_df=True):
    url = "https://api.pancakeswap.info/api/v2/summary"
    r = requests.get(url).json()
    data = r.get('data', None)
    upd = r.get('updated_at') / 1000
    upd_dt = datetime.datetime.fromtimestamp(upd)

    if as_df:
        df = pd.DataFrame.from_dict(data, orient='index')
        df = toFloatPartial(df)
        df['updated'] = upd_dt
        return df
    else:
        return r


def pcsTokens(as_df=True):
    """get all token listed in pancakeswap

    Args:
        as_df (bool, optional): if True (default), return is a dataframe, else is a dictionary

    Returns:
        DataFrame with next columns: name  symbol  price   price_BNB   updated

    """
    # ultimo precio y volumen de base/quote de todos los pares

    url = "https://api.pancakeswap.info/api/v2/tokens"
    r = requests.get(url).json()
    data = r.get('data', None)
    upd = r.get('updated_at') / 1000
    upd_dt = datetime.datetime.fromtimestamp(upd)

    if as_df:
        df = pd.DataFrame.from_dict(data, orient='index')
        df = toFloatPartial(df)
        df['updated'] = upd_dt
        return df
    else:
        return r


def pcsPairs(as_df=True):
    """get top 1000 pancakeswap pairs LP order by reserves

    Args:
        as_df (bool, optional): if True (default), return is a dataframe, else is a dictionary

    Returns:
        DataFrame with next columns: 'pair_address', 'base_name', 'base_symbol', 'base_address',
       'quote_name', 'quote_symbol', 'quote_address', 'price', 'base_volume',
       'quote_volume', 'liquidity', 'liquidity_BNB', 'updated'
    """

    url = "https://api.pancakeswap.info/api/v2/pairs"
    r = requests.get(url).json()
    data = r.get('data', None)
    upd = r.get('updated_at') / 1000
    upd_dt = datetime.datetime.fromtimestamp(upd)

    if as_df:
        df = pd.DataFrame.from_dict(data, orient='index')
        df = toFloatPartial(df)
        df['updated'] = upd_dt
        return df
    else:
        return r


def pcsTokenInfo(search):
    """get info from a token

    Args:
        search (string): Token symbol or contract address

    Returns:
        Dict:
        {
         'name': 'Wrapped BNB',
         'symbol': 'WBNB',
         'price': '524.5429',
         'price_BNB': '1'
         }
    """
    search = 'WBNB' if search.upper() == 'BNB' else search
    url = "https://api.pancakeswap.info/api/v2/tokens"
    r = requests.get(url).json()
    data = r.get('data', None)
    res = f"Not found: {search}"
    for contract, values in data.items():
        if search.upper() == values['symbol'].upper() or search.upper() == contract.upper():
            res = data[contract]
            break

    return res


def pcsPairInfo(base, quote):
    """get info from a token pair LP

    Args:
        base (string): Base LP token, ie "CAKE"
        quote (string): Quote LP token, ie "BNB"
        its the same if you call pcsPAirInfo('cake', 'bnb') or pcsPAirInfo('bnb', 'cake')
    Returns:
        Dict: {
                 'pair_address': '0xA527a61703D82139F8a06Bc30097cC9CAA2df5A6',
                 'base_name': 'PancakeSwap Token',
                 'base_symbol': 'Cake',
                 'base_address': '0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82',
                 'quote_name': 'Wrapped BNB',
                 'quote_symbol': 'WBNB',
                 'quote_address': '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
                 'price': '0.04311198194009326668',
                 'base_volume': '22248744.85',
                 'quote_volume': '934856.36',
                 'liquidity': '982769040.63',
                 'liquidity_BNB': '1878155.84'
                }
                * price is actually a ratio between base/quote tokens
    """
    url = "https://api.pancakeswap.info/api/pairs"
    r = requests.get(url).json()
    data = r.get('data', None)
    res = f"Not found: {base}-{quote}"
    base = 'WBNB' if base.upper() == 'BNB' else base
    quote = 'WBNB' if quote.upper() == 'BNB' else quote

    for contract, values in data.items():
        base_ = base.upper() == values['base_symbol'].upper()
        quote_ = quote.upper() == values['quote_symbol'].upper()
        base_cross = base.upper() == values['quote_symbol'].upper()
        quote_cross = quote.upper() == values['base_symbol'].upper()

        if (base_ and quote_) or (base_cross and quote_cross):
            res = data[contract]
            break

    return res


# from scipy import interpolate
import matplotlib.cm as cm


# def iloss_simulate(base_token, quote_token, value=100, base_pct_chg=0, quote_pct_chg=0):
#     """Calculate simulated impermanent loss from an initial value invested, get real time prices from pancakeswap API
#         This method create a 3D interpolated surface for impermanent loss and initial/final value invested
#
#     Args:
#         base_token (string): Pair first token, ie CAKE
#         quote_token (string): Pais second token, ie BNB
#         value (int, optional): Value investen in LP default=100
#         base_pct_chg (int, optional): value assming will change first token of LP pair, ie 10 (for +10% change)
#         quote_pct_chg (int, optional): value assming will change first token of LP pair, ie -30 (for -30% change)
#
#     Returns:
#         tuple (value_f, iloss): final value of value invested, and decimal impermanent loss
#     """
#     base_token = 'WBNB' if base_token.upper() == 'BNB' else base_token
#     quote_token = 'WBNB' if quote_token.upper() == 'BNB' else quote_token
#
#     # get real time prices
#     tokens = pcsTokens()
#     px_base = float(tokens.loc[tokens.symbol.str.upper() == base_token.upper()].price)
#     px_quote = float(tokens.loc[tokens.symbol.str.upper() == quote_token.upper()].price)
#
#     # Prepare grid
#     q_base, q_quote = (value / 2) / px_base, (value / 2) / px_quote
#     px_base, px_quote, q_base, q_quote
#     pxs_base = [px_base * i / 100 for i in range(1, 301)]
#     pxs_quote = [px_quote * i / 100 for i in range(1, 301)]
#     rows = []
#     for px_b in pxs_base:
#         for px_q in pxs_quote:
#             ratio = (px_b / px_base) / (px_q / px_quote)
#             iloss = 2 * (ratio ** 0.5 / (1 + ratio)) - 1
#
#             row = {'px_base': px_b, 'px_quote': px_q,
#                    'ratio': (px_b / px_base) / (px_q / px_quote),
#                    'impremante_loss': iloss}
#             rows.append(row)
#     df = pd.DataFrame(rows)
#     df_ok = df.loc[:, ['px_base', 'px_quote', 'impremante_loss']]
#     df_ok = df_ok.replace('NaN', np.nan).dropna()
#
#     if all(isinstance(i, (int, float)) for i in (value, base_pct_chg, quote_pct_chg)):
#         px_base_f = px_base * (1 + base_pct_chg / 100)
#         px_quote_f = px_quote * (1 + quote_pct_chg / 100)
#         ratio = (px_base_f / px_base) / (px_quote_f / px_quote)
#         iloss = 2 * (ratio ** 0.5 / (1 + ratio)) - 1
#         value_f = (px_base_f * q_base + px_quote_f * q_quote) * (iloss + 1)
#     else:
#         px_base_f, px_quote_f = px_base, px_quote
#         iloss = 0
#         value_f = None
#         print('must input numerical amount and pct change for base and quote to calculations of final value')
#
#     # Ploting surface
#     fig = plt.figure(figsize=(8, 8))
#     x1 = np.linspace(df_ok['px_base'].min(), df_ok['px_base'].max(), len(df_ok['px_base'].unique()))
#     y1 = np.linspace(df_ok['px_quote'].min(), df_ok['px_quote'].max(), len(df_ok['px_quote'].unique()))
#     x2, y2 = np.meshgrid(x1, y1)
#     Z = interpolate.griddata((df_ok['px_base'], df_ok['px_quote']), df_ok['impremante_loss'], (x2, y2))
#     Z[np.isnan(Z)] = df_ok.impremante_loss.mean()
#     ax = plt.axes(projection='3d', alpha=0.2)
#     ax.plot_wireframe(x2, y2, Z, color='tab:blue', lw=1, cmap='viridis', alpha=0.6)
#
#     # Start values ploting
#     xmax = df_ok.px_base.max()
#     ymax = df_ok.px_quote.max()
#     ax.plot([px_base, px_base], [0, px_quote], [-1, -1], ls='--', c='k', lw=1)
#     ax.plot([px_base, px_base], [px_quote, px_quote], [0, -1], ls='--', c='k', lw=1)
#     ax.plot([px_base, 0], [px_quote, px_quote], [-1, -1], ls='--', c='k', lw=1)
#
#     # End values ploting
#     ax.plot([px_base_f, px_base_f], [0, px_quote_f], [-1, -1], ls='--', c='gray', lw=1)
#     ax.plot([px_base_f, px_base_f], [px_quote_f, px_quote_f], [iloss, -1], ls='--', c='gray', lw=1)
#     ax.plot([px_base_f, 0], [px_quote_f, px_quote_f], [-1, -1], ls='--', c='gray', lw=1)
#     ax.plot([px_base_f, px_base_f], [px_quote_f, ymax], [iloss, iloss], ls='--', c='gray', lw=1)
#     ax.plot([px_base_f, 0], [ymax, ymax], [iloss, iloss], ls='--', c='gray', lw=1)
#
#     # Plot settings
#     # Colorbar only for plot_surface() method instead plot_wireframe()
#     # m = cm.ScalarMappable(cmap=cm.viridis)
#     # m.set_array(df_ok['impremante_loss'])
#     # plt.colorbar(m, fraction=0.02, pad=0.1)
#     x, y, z = (px_base, px_quote, .05)
#     p = ax.scatter(x, y, z, c='k', marker='v', s=300)
#     ax.set_title('Impermanent Loss 3D Surface', y=0.95)
#     ax.set_xlabel(f'Price {base_token}')
#     ax.set_ylabel(f'Price {quote_token}')
#     ax.set_zlabel('Impremante loss')
#     ax.view_init(elev=25, azim=240)  # start view angle
#
#     print(f"\nStart value USD {value:.0f}, {base_token} USD {px_base:.2f}, {quote_token} USD {px_quote:.2f}")
#     print(f"\nResults assuming {base_token.upper()} {base_pct_chg}%, and {quote_token.upper()} {quote_pct_chg}%")
#     print(f"End value estimate USD {value_f:.0f}, iloss: {iloss:.2%}")
#     plt.show()
#
#     return value_f, iloss