"""
A simulation runs trades against Curve pools, using a strategy that may
utilize different types of informed or noise trades.

The :mod:`simulation pipeline framework <curvesim.pipelines>` allows the
user to build custom strategies for simulation.

Most users will want to use the `autosim` function, which supports
"optimal" arbitrages via the
:func:`volume-limited arbitrage pipeline <curvesim.pipelines.vol_limited_arb.pipeline>`.
The primary use-case is to determine optimal amplitude (A) and fee
parameters given historical price and volume feeds.
"""
from curvesim.logging import get_logger
from curvesim.pipelines.vol_limited_arb import DEFAULT_PARAMS
from curvesim.pipelines.vol_limited_arb import pipeline as volume_limited_arbitrage
from curvesim.pool_data import get_metadata
from curvesim.utils import get_pairs

logger = get_logger(__name__)


def autosim(
    pool=None,
    chain="mainnet",
    pool_metadata=None,
    pool_data_cache=None,
    env="prod",
    **kwargs,
):
    """
    The autosim() function simulates existing Curve pools with a range of
    parameters (e.g., the amplitude parameter, A, and/or the exchange fee).

    The function fetches pool properties (e.g., current pool size) and 2
    months of price/volume data and runs multiple simulations in parallel.

    Curve pools from any chain supported by the Convex Community Subgraphs
    can be simulated directly by inputting the pool's address.

    Parameters
    ----------
    pool: str, optional
        This 0x-prefixed string identifies the pool by address.

        .. note::
            Either `pool` or `pool_metadata` must be provided.

    chain: str, default='mainnet'
        Identifier for blockchain or layer2.  Supported values are:
            "mainnet", "arbitrum", "optimism", "fantom", "avalanche"
            "matic", "xdai"

    pool_metadata: PoolMetaDataInterface, optional
        Pool state and metadata necessary to instantiate a pool object.

        .. note::
            Either `pool` or `pool_metadata` must be provided.

    pool_data_cache: PoolDataCache, optional
        Cached data used in sims.  Useful for replication of results and
        avoiding re-fetches of data.

    A: int or iterable of int, optional
        Amplification coefficient.  This controls the curvature of the
        stableswap bonding curve.  Increased values makes the curve
        flatter in a greater neighborhood of equal balances.

        Defaults to ``[int(2 ** (a / 2)) for a in range(12, 28)]``.

        For basepool, use **A_base**.

    D: int, optional
        Total pool liquidity given in 18 decimal precision.
        Defaults to on-chain data.

        For basepool, use **D_base**.

    tokens: int, optional
        Total LP token supply.
        Defaults to on-chain data.

        For basepool, use **tokens_base**.

    fee: int or iterable of int, optional
        Fees taken for both liquidity providers and the DAO.

        Units are in fixed-point so that 10**10 is 100%,
        e.g. 4 * 10**6 is 4 bps and 2 * 10**8 is 2%.

        Defaults to ``list(range(1000000, 5000000, 1000000))``.

        For basepool, use **fee_base**.

    fee_mul : int
        Fee multiplier for dynamic fee pools.

        For basepool, use **fee_mul_base**.

    admin_fee : int, default=0 * 10**9
        Fees taken for the DAO.  For factory pools, it is half of the total
        fees, as was typical for previous non-factory pools.

        Units are fixed-point percentage of `fee`, e.g. 5 * 10**9 is
        50% of the total fees.

    test: bool, default=False
        Overrides variable_params to use four test values:

        .. code-block::

            {"A": [100, 1000], "fee": [3000000, 4000000]}

    days: int, default=60
        Number of days to fetch data for.

    src: str, default='coingecko'
        Valid values for data source are 'coingecko' or 'local'

    data_dir: str, default='data'
        Relative path to saved data folder.

    vol_mult : dict, float, or int, default computed from data
        Value(s) multiplied by market volume to specify volume limits
        (overrides vol_mode).

        dict should map from trade-pair tuples to values, e.g.:

        .. code-block::

            {('DAI', 'USDC'): 0.1, ('DAI', 'USDT'): 0.1, ('USDC', 'USDT'): 0.1}

    vol_mode : int, default=1
        Modes for limiting trade volume.
        1: limits trade volumes proportionally to market volume for each pair
        2: limits trade volumes equally across pairs
        3: mode 2 for trades with meta-pool asset, mode 1 for basepool-only trades

    ncpu : int, default=os.cpu_count()
        Number of cores to use.

    env: str, default='prod'
        Environment for the Curve subgraph, which pulls pool and volume snapshots.

    Returns
    -------
    dict
        Dictionary of results, each value being a pandas.Series.
    """
    assert any([pool, pool_metadata]), "Must input 'pool' or 'pool_metadata'"

    pool_metadata = pool_metadata or get_metadata(pool, chain, env)
    p_var, p_fixed, kwargs = _parse_arguments(pool_metadata, **kwargs)

    results = volume_limited_arbitrage(
        pool_metadata,
        pool_data_cache,
        variable_params=p_var,
        fixed_params=p_fixed,
        **kwargs,
    )

    return results


def _parse_arguments(pool_metadata, **kwargs):
    pool_args = [
        "A",
        "D",
        "balances",
        "fee",
        "fee_mul",
        "tokens",
        "admin_fee",
        "gamma",
        "fee_gamma",
        "mid_fee",
        "out_fee",
    ]
    pool_args += [arg + "_base" for arg in pool_args[:-1]]

    variable_params = {}
    fixed_params = {}
    rest_of_params = {}
    defaults = DEFAULT_PARAMS.copy()

    for key, val in kwargs.items():
        if key in defaults:
            del defaults[key]

        if key in pool_args:
            if isinstance(val, int):
                fixed_params[key] = val

            elif all(isinstance(v, int) for v in val):
                variable_params[key] = val

            else:
                raise TypeError(f"Argument {key} must be an int or iterable of ints")

        elif key == "vol_mult" and isinstance(val, (int, float)):
            coin_pairs = get_pairs(pool_metadata.coin_names)
            rest_of_params[key] = dict.fromkeys(coin_pairs, val)

        else:
            rest_of_params[key] = val

    variable_params = variable_params or defaults

    return variable_params, fixed_params, rest_of_params
