import logging
import math

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

log = logging.getLogger(__name__)

# UNISWAP_GAS=0
# LMSR_GAS=0
UNISWAP_GAS=115_000
LMSR_GAS=150_000
ETH_PRICE=4000
UNISWAP_GAS_COST=UNISWAP_GAS*ETH_PRICE/1e9
LMSR_GAS_COST=LMSR_GAS*ETH_PRICE/1e9


print(f'   LMSR gas: ${LMSR_GAS_COST:.2}')
print(f'Uniswap gas: ${UNISWAP_GAS_COST:.2}')


def lmsr_swap_amount_out(
    balances,
    amount_in,
    token_in_index,
    token_out_index,
    lp_fee,
    kappa,
):
    """
    Compute the LMSR kernel fee-free amountOut for swapping `amount_in` of token `token_in_index`
    into token `token_out_index`, applying lp_fee to the input amount (i.e., amount_in_net = amount_in * (1 - lp_fee)).
    Uses native Python floats for performance and simplicity.

    Notes on congruence with PartyPool / LMSRStabilized:
    - The kernel formula implemented here matches the LMSR closed-form:
        amountOut = b * ln(1 + r0 * (1 - exp(-a / b)))
      where r0 = exp((q_i - q_j) / b) and b = kappa * S (S = sum balances).
    - lp_fee is applied to the input before the kernel (fee-on-input), matching PartyPool's placement.
    - This function uses continuous float arithmetic and does NOT emulate
      PartyPool's integer/unit conversions or rounding (floor/ceil) and ppm fee quantization.

    Parameters:
    - balances: iterable of per-token balances (numbers). These represent q_i in internal units.
    - amount_in: input amount supplied by swapper (before fees).
    - token_in_index: index of the input token i.
    - token_out_index: index of the output token j.
    - lp_fee: fractional LP fee applied to the input (e.g., 0.003).
    - kappa: liquidity parameter κ (must be positive, in same units as balances).

    Returns:
    - float: net amountOut (exclusive of fees) the swapper receives from the pool (capped at pool balance).
    """
    # Normalize and validate inputs (convert to floats)
    try:
        q = [float(x) for x in balances]
        a_in = float(amount_in)
        lpf = float(lp_fee)
        k = float(kappa)
    except (TypeError, ValueError) as e:
        raise ValueError("Invalid numeric input") from e

    n = len(q)
    if not (0 <= token_in_index < n and 0 <= token_out_index < n):
        raise IndexError("token indices out of range")

    if a_in <= 0.0:
        return 0.0

    if k <= 0.0:
        raise ValueError("kappa must be positive")

    # Size metric S = sum q_i
    S = sum(q)
    if S <= 0.0:
        raise ValueError("size metric (sum balances) must be positive")

    # b = kappa * S
    b = k * S
    if b <= 0.0:
        raise ValueError("computed b must be positive")

    # Apply LP fee on the input amount (kernel is fee-free; wrapper handles fees)
    if not (0.0 <= lpf < 1.0):
        raise ValueError("lp_fee must be in [0, 1)")
    a_net = a_in * (1.0 - lpf)
    if a_net <= 0.0:
        return 0.0

    qi = q[token_in_index]
    qj = q[token_out_index]

    # Guard: output asset must have non-zero effective reserve
    if qj <= 0.0:
        # No available output to withdraw
        return 0.0

    # Compute r0 = exp((q_j - q_i) / b) so small-trade out/in ≈ marginal price p_j/p_i
    try:
        r0 = math.exp((qj - qi) / b)
    except OverflowError:
        raise ArithmeticError("exponential overflow in r0 computation")

    # Compute a/b
    a_over_b = a_net / b

    # exp(-a/b)
    try:
        exp_neg = math.exp(-a_over_b)
    except OverflowError:
        # If exp would underflow/overflow, treat exp(-a/b) as 0 in extreme case
        exp_neg = 0.0

    # inner = 1 + r0 * (1 - exp(-a/b))
    inner = 1.0 + r0 * (1.0 - exp_neg)

    # If inner <= 0, cap to available balance qj
    if inner <= 0.0:
        return float(qj)

    # amountOut = b * ln(inner)
    try:
        ln_inner = math.log(inner)
    except (ValueError, OverflowError):
        # Numeric issue computing ln; be conservative and return 0
        return 0.0

    amount_out = b * ln_inner

    # Safety: non-positive output -> return zero
    if amount_out <= 0.0:
        return 0.0

    # Cap output to pool's current balance qj (cannot withdraw more than available)
    if amount_out > qj:
        return float(qj)

    return float(amount_out)

def lmsr_marginal_price(balances, base_index, quote_index, kappa):
    """
    Compute the LMSR marginal price ratio p_quote / p_base for the given balances state.

    Formula:
      b = kappa * S, where S = sum(balances)
      price = exp((q_quote - q_base) / b)

    Parameters:
    - balances: iterable of per-token balances (q_i)
    - base_index: index of the base token
    - quote_index: index of the quote token
    - kappa: liquidity parameter κ (must be positive)

    Returns:
    - float: marginal price p_quote / p_base
    """
    try:
        q = [float(x) for x in balances]
        k = float(kappa)
    except (TypeError, ValueError) as e:
        raise ValueError("Invalid numeric input") from e

    n = len(q)
    if not (0 <= base_index < n and 0 <= quote_index < n):
        raise IndexError("token indices out of range")
    if k <= 0.0:
        raise ValueError("kappa must be positive")

    S = sum(q)
    if S <= 0.0:
        raise ValueError("size metric (sum balances) must be positive")

    b = k * S
    if b <= 0.0:
        raise ValueError("computed b must be positive")

    return float(math.exp((q[quote_index] - q[base_index]) / b))



def compare(file, tvl, fee, kappa):
    d = pd.read_csv(file)
    d.columns = ['block', 'price0', 'price1', 'in0', 'out0', 'rate']

    # New approach: derive bases from initial external balances (assuming equal-valued deposits)
    # This matches the Solidity implementation and eliminates the κ·ln(price) constraint
    p0 = float(d.iloc[0].price0)

    # Set external balances assuming equal values: if token0 = B0 and token1 = B1,
    # and they have equal value, then B0 * price0 = B1 * price1 = V (value per asset)
    # For simplicity, choose B0 such that the total value is tvl, then B1 = B0 * price0
    total_value = float(tvl)
    # Since B0 * price0 + B1 = total_value and B1 = B0 * price0, we get:
    # B0 * price0 + B0 * price0 = total_value, so B0 = total_value / (2 * price0)
    B0 = total_value / (2.0 * p0)  # external balance of token 0
    B1 = B0 * p0                   # external balance of token 1 (equal value)

    external_balances = [B0, B1]

    # Derive bases: set base_i = B_i so that q_i = B_i / base_i = 1.0 internally
    bases = [B0, B1]

    # Internal balances: q_i = external_balance_i / base_i ≈ 1.0
    q0 = B0 / bases[0]  # ≈ 1.0
    q1 = B1 / bases[1]  # ≈ 1.0

    balances = [q0, q1]
    print(f"External balances: {external_balances}")
    print(f"Bases: {bases}")
    print(f"Internal balances: {balances}")
    # Convert external input amounts to internal for LMSR calculations, then convert results back
    X = np.geomspace(1, 1_000_000, 100)
    orig_price = lmsr_marginal_price(balances, 0, 1, kappa)

    # Convert X to internal amounts, compute swap, then convert back to external
    in_out = []
    for amount_in_external in X:
        # Convert external input to internal units
        amount_in_internal = amount_in_external / bases[0]  # input token 0

        # Compute swap in internal units
        amount_out_internal = lmsr_swap_amount_out(balances, amount_in_internal, 0, 1, fee, kappa)

        # Convert output back to external units
        amount_out_external = amount_out_internal * bases[1]  # output token 1

        in_out.append((float(amount_in_external), float(amount_out_external)))

    print(f"Sample internal/external conversions: {in_out[:3]}")

    # Compute initial marginal price in external units
    # Internal price is exp((q1 - q0)/b), external price needs conversion by bases[1]/bases[0]
    orig_price_external = orig_price * (bases[1] / bases[0])

    # Relative execution price deviation from the initial marginal price:
    # slippage = |(amount_out/amount_in)/orig_price_external - 1|
    eps = 1e-12
    Y = [max(eps, abs((amount_out / amount_in) / orig_price_external - 1.0))
         for amount_in, amount_out in in_out]
    plt.plot(X, Y, label=f'LMSR {fee:.2%} κ={kappa:.2g}', color='cornflowerblue')

    # Uniswap execution price deviation from its initial quoted price:
    # slippage = |(out/in)/initial_price - 1|
    uniswap_exec_price0 = d.out0 / d.in0
    uniswap_slippage0 = (uniswap_exec_price0 / d.iloc[0].price0 - 1.0).abs().clip(lower=1e-12)
    uniswap_fee = round(uniswap_slippage0.iloc[0], 6)
    plt.plot(d.in0, uniswap_slippage0, label=f'Uniswap {uniswap_fee:.2%}', color='hotpink')
    # uniswap_slippage1 = |(out1/in1)/price1 - 1|
    # plt.plot(d.in1, (d.out1 / d.in1 / d.iloc[0].price1 - 1.0).abs().clip(lower=1e-12), label='CP1')

    # Interpolate Uniswap slippage to match LMSR x-coordinates
    interp_uniswap = np.interp(X, d.in0, uniswap_slippage0)
    mask = Y < interp_uniswap
    plt.fill_between(X, 0, 1, where=mask, alpha=0.2, color='green')

    plt.xscale('log')
    plt.yscale('log')
    plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: '{:g}'.format(x)))
    plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.2%}'.format(y)))
    plt.gca().set_ylim(top=.1)
    plt.xlabel('Input Amount')
    plt.ylabel('Slippage')
    plt.title('Pool Slippages')
    plt.grid(True)
    plt.legend()
    plt.show()


def plot_kappa():
    X = np.geomspace(1, 10_000_000, 100)
    for kappa in np.geomspace(0.01, 100, 8):
        balance0 = 1_000_000  # estimated from the production pool
        balances = [balance0, balance0]
        Y = [1 -
             lmsr_swap_amount_out(balances, float(amount_in), 0, 1, 0.000010, kappa)
             / amount_in
             for amount_in in X]
        plt.plot(X / balance0, Y, label=f'{kappa:f}')
    plt.xscale('log')
    plt.yscale('log')
    plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: '{:.2f}'.format(10000*x)))
    plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.2%}'.format(y)))
    plt.xlabel('Input Amount (basis points of initial balance)')
    plt.ylabel('Slippage')
    plt.title('Pool Slippages by Kappa')
    plt.grid(True)
    plt.legend()
    plt.show()


if __name__ == '__main__':
    # compare('uni4_quotes/swap_results_block_23640998.csv')
    compare('uni4_quotes/ETH-USDC-30.csv', 53_000_000, 0.0025, 0.00025)
    # compare('uni4_quotes/ETH-USDC-30.csv', 100_000, 0.0025, 0.00025)
    # plot_kappa()
