import logging
import math
from itertools import combinations

log = logging.getLogger(__name__)

logging.basicConfig(level=logging.INFO)

# Now interpret the `weights` mapping as the desired pair fee (in bps) vs USDC.
# Examples:
#  - USDC: 0.0   -> trading USDC<>USDC costs 0 bps
#  - USDT: 0.10  -> trading USDT<>USDC costs ~0.1 bps
#  - WIF: 30.0   -> trading WIF<>USDC costs ~30 bps
#
# For arbitrary pair A<>B we compose per-asset fees multiplicatively:
#   f_eff = 1 - (1 - f_A) * (1 - f_B)
# which is ≈ f_A + f_B for small fees.

STABLECOIN_RATE = 0.01 # bps
X0 = STABLECOIN_RATE / 2
targets = {
    'USDT': STABLECOIN_RATE,
    'USDC': STABLECOIN_RATE,
    'WBTC': 3.0,    # ~3 bps vs USDC
    'WETH': 5.0,    # ~5 bps vs USDC
    'WIF': 30.0,    # ~30 bps vs USDC
}
weights = {k:v-X0 for k,v in targets.items()}

# UI / safety parameters (bps)
# cap_pair_fee_bps: global cap on the composed pair fee to avoid degenerate values (set to None to disable)
cap_pair_fee_bps = None  # safety cap (bps); adjust or set to None to disable

# Compute geometric mean "base" of weights (kept for diagnostics)
def geometric_mean(vals):
    # Guard: all weights must be positive (allow zero for USDC)
    for v in vals:
        if v < 0:
            raise ValueError("All weights must be non-negative")
    # exclude zeros when computing geometric mean to avoid exact zero product
    pos_vals = [v for v in vals if v > 0]
    if not pos_vals:
        return 0.0
    return math.prod(pos_vals) ** (1.0 / len(pos_vals))

base = geometric_mean(list(weights.values()))
ref_asset = 'USDC'

# Convert UI bps -> fractional form for internal calculations
def bps_to_frac(bps_val):
    return bps_val / 10_000.0

log.info(
    f"Fee model (weights are target bps vs USDC): ref_asset={ref_asset}, base_geo={base:.6f}"
)

def clamp_nonneg(x):
    return x if x >= 0.0 else 0.0

def clamp_cap_frac(x_frac):
    """Clamp a fractional fee to configured cap_pair_fee_bps if set"""
    if cap_pair_fee_bps is None:
        return x_frac
    cap_frac = bps_to_frac(cap_pair_fee_bps)
    return min(x_frac, cap_frac)

def per_asset_fee(w_bps):
    """
    Interpret `w_bps` directly as the target bps vs USDC for that asset and
    return the fractional per-asset fee (clamped).

    So for asset A:
      f_A = bps_to_frac(weights[A])
    """
    return clamp_nonneg(bps_to_frac(w_bps))

def fee_for_pair(w_in_bps, w_out_bps):
    """
    Compute effective fee fraction for a swap i->j using multiplicative composition
    of per-asset fees:

      f_i = per_asset_fee(w_in_bps)
      f_j = per_asset_fee(w_out_bps)
      f_eff = 1 - (1 - f_i) * (1 - f_j)

    Returns tuple (f_eff_frac, f_i_frac, f_j_frac).
    """
    f_i = per_asset_fee(w_in_bps)
    f_j = per_asset_fee(w_out_bps)

    # multiplicative composition (accurate and gas-cheap counterpart)
    f_eff = 1.0 - (1.0 - f_i) * (1.0 - f_j)

    # apply global cap if configured
    f_eff = clamp_cap_frac(f_eff)
    return clamp_nonneg(f_eff), f_i, f_j

def pct(x):
    return f"{100.0 * x:.6f}%"

def bps(x):
    # x expected as fraction; convert to bps (1 bps = 1/10000)
    return f"{10_000.0 * x:.4f} bps"

if __name__ == "__main__":
    names = list(weights.keys())
    vals = list(weights.values())

    # Warn if any single-asset implied fee would produce large numbers
    alert_threshold_bps = 1000.0
    alerts = []
    max_w = max(weights.values())
    min_w = min(weights.values())
    max_diff = max_w - min_w
    if max_w > alert_threshold_bps:
        alerts.append(("single_asset_too_large_bps", max_w))
    if alerts:
        log.warning("Some fee configuration may produce large values (bps): %s", alerts)

    # Per-asset diagnostic: show per-asset fee (interpreted vs USDC)
    for n, w in weights.items():
        f_token = per_asset_fee(w)
        print(f"{n:>12} {bps(f_token):>12}")
    print()

    for (name_a, w_a), (name_b, w_b) in combinations(weights.items(), 2):
        f_eff_ab, f_i_ab, f_j_ab = fee_for_pair(w_a, w_b)

        print(
            f"{name_a+'-'+name_b:>12} {bps(f_eff_ab):>12}"
        )

    # -- Solidity (ABDK Q64.64) output --
    # Print derived per-asset fees and cap as Solidity int128 Q64.64 constants that can be pasted
    # into a Solidity file using ABDK's 64.64 fixed-point representation (int128 constants).
    print("\n// Solidity (ABDK Q64.64) constants - generated by research/fees.py")
    print("// Per-asset fees (FEE_<SYMBOL>) are Q64.64 int128 literals representing the fractional fee")
    print("// Example: int128 internal constant FEE_USDT = 0x012345...; // comment\n")

    # Helper: order tokens consistently for array examples
    token_names = list(weights.keys())

    for name in token_names:
        w_bps = weights[name]
        t_bps = targets[name]
        f_frac = per_asset_fee(w_bps)
        q64_int = int(round(f_frac * (1 << 64)))
        hexstr = hex(q64_int)
        print(f"int128 internal constant FEE_{name} = {hexstr}; // {bps(f_frac)}")

    # Cap constant if configured
    if cap_pair_fee_bps is not None:
        cap_frac = bps_to_frac(cap_pair_fee_bps)
        cap_q64 = int(round(cap_frac * (1 << 64)))
        print(f"int128 internal constant CAP_PAIR_FEE_Q64 = {hex(cap_q64)}; // cap {cap_pair_fee_bps} bps")

    # Example how to assemble into an array in the same order as `token_names`.
    print("\n// Example: build a per-asset fee array (same ordering as token_names list above)")
    print(f"// token order: {token_names}")
    print(f"int128[] memory perAssetFeesQ64 = new int128[]({len(token_names)});")
    for idx, name in enumerate(token_names):
        print(f"perAssetFeesQ64[{idx}] = FEE_{name};")

    print("\n// End of generated Solidity constants\n")

    # Notes:
    # - weights[name] should be set to the desired bps (not fraction) vs USDC.
    # - Per-asset fraction f_token = bps_to_frac(weights[name]) and
    #   composed pair fee is f_eff = 1 - (1 - f_i) * (1 - f_j).
    # - This keeps USDC as the reference (weights['USDC'] == 0.0).

