from __future__ import annotations

from decimal import Decimal, getcontext

import pytest
from eth_account import Account

import wayfinder_paths.adapters.aerodrome_common as aerodrome_common_module
from wayfinder_paths.adapters.aerodrome_slipstream_adapter.adapter import (
    AerodromeSlipstreamAdapter,
)
from wayfinder_paths.core.constants.aerodrome_abi import (
    AERODROME_VOTER_ABI,
    AERODROME_VOTING_ESCROW_ABI,
)
from wayfinder_paths.core.constants.aerodrome_slipstream_abi import (
    AERODROME_SLIPSTREAM_CL_GAUGE_ABI,
    AERODROME_SLIPSTREAM_CL_POOL_ABI,
    AERODROME_SLIPSTREAM_NPM_ABI,
)
from wayfinder_paths.core.constants.aerodrome_slipstream_contracts import (
    AERODROME_SLIPSTREAM_BY_CHAIN,
)
from wayfinder_paths.core.constants.chains import CHAIN_ID_BASE
from wayfinder_paths.core.utils import web3 as web3_utils
from wayfinder_paths.testing.gorlami import gorlami_configured

pytestmark = pytest.mark.skipif(
    not gorlami_configured(),
    reason="api_key not configured (needed for gorlami fork proxy)",
)

CHAIN_ID = CHAIN_ID_BASE
ENTRY = AERODROME_SLIPSTREAM_BY_CHAIN[CHAIN_ID]
AERO = str(ENTRY["aero"])
WETH = str(ENTRY["weth"])

getcontext().prec = 80


def _make_adapter(acct: Account) -> AerodromeSlipstreamAdapter:
    async def sign_cb(tx: dict) -> bytes:
        signed = acct.sign_transaction(tx)
        return signed.raw_transaction

    return AerodromeSlipstreamAdapter(
        sign_callback=sign_cb,
        wallet_address=acct.address,
    )


async def _ensure_fork(gorlami) -> str:
    async with web3_utils.web3_from_chain_id(CHAIN_ID) as web3:
        assert await web3.eth.chain_id == int(CHAIN_ID)
    fork_info = gorlami.forks.get(str(CHAIN_ID))
    assert fork_info is not None
    return fork_info["fork_id"]


async def _try_time_travel(gorlami, fork_id: str, *, target_ts: int) -> bool:
    candidates = [
        ("evm_setNextBlockTimestamp", [target_ts]),
        ("anvil_setNextBlockTimestamp", [target_ts]),
        ("hardhat_setNextBlockTimestamp", [target_ts]),
    ]
    for method, params in candidates:
        try:
            await gorlami.send_rpc(fork_id, method, params)
            await gorlami.send_rpc(fork_id, "evm_mine", [])
            return True
        except Exception:
            continue

    try:
        block = await gorlami.send_rpc(
            fork_id, "eth_getBlockByNumber", ["latest", False]
        )
        now = int(block.get("timestamp") or "0x0", 16) if isinstance(block, dict) else 0
    except Exception:
        now = 0

    delta = max(0, int(target_ts) - int(now))
    if delta <= 0:
        return True

    for method in ("evm_increaseTime", "anvil_increaseTime", "hardhat_increaseTime"):
        try:
            await gorlami.send_rpc(fork_id, method, [delta])
            await gorlami.send_rpc(fork_id, "evm_mine", [])
            return True
        except Exception:
            continue
    return False


async def _move_to_safe_vote_window(gorlami, fork_id: str) -> bool:
    block = await gorlami.send_rpc(fork_id, "eth_getBlockByNumber", ["latest", False])
    now = int(block.get("timestamp") or "0x0", 16) if isinstance(block, dict) else 0

    epoch_start = (now // WEEK_SECONDS) * WEEK_SECONDS
    window_start = epoch_start + EPOCH_SPECIAL_WINDOW_SECONDS + 5
    window_end = epoch_start + WEEK_SECONDS - EPOCH_SPECIAL_WINDOW_SECONDS - 5

    if now < window_start:
        target_ts = window_start
    elif now >= window_end:
        target_ts = epoch_start + WEEK_SECONDS + EPOCH_SPECIAL_WINDOW_SECONDS + 5
    else:
        return True

    return await _try_time_travel(gorlami, fork_id, target_ts=target_ts)


async def _move_to_next_safe_vote_window(gorlami, fork_id: str) -> bool:
    block = await gorlami.send_rpc(fork_id, "eth_getBlockByNumber", ["latest", False])
    now = int(block.get("timestamp") or "0x0", 16) if isinstance(block, dict) else 0
    next_epoch_start = ((now // WEEK_SECONDS) + 1) * WEEK_SECONDS
    target_ts = next_epoch_start + EPOCH_SPECIAL_WINDOW_SECONDS + 5
    return await _try_time_travel(gorlami, fork_id, target_ts=target_ts)


def _round_tick_down(tick: int, spacing: int) -> int:
    return (int(tick) // int(spacing)) * int(spacing)


async def _discover_live_market(
    adapter: AerodromeSlipstreamAdapter,
) -> tuple[dict, str]:
    ok, matches = await adapter.find_pools(tokenA=AERO, tokenB=WETH)
    assert ok is True, matches
    assert matches, "No Slipstream AERO/WETH pools discovered"

    async with web3_utils.web3_from_chain_id(CHAIN_ID) as web3:
        voter = web3.eth.contract(
            address=web3.to_checksum_address(str(ENTRY["voter"])),
            abi=AERODROME_VOTER_ABI,
        )
        for match in matches:
            pool = web3.to_checksum_address(match["pool"])
            gauge = await voter.functions.gauges(pool).call(block_identifier="latest")
            if str(gauge).lower() == "0x0000000000000000000000000000000000000000":
                continue
            is_alive = await voter.functions.isAlive(gauge).call(
                block_identifier="latest"
            )
            if is_alive:
                return match, web3.to_checksum_address(gauge)

    raise AssertionError("No alive Slipstream AERO/WETH gauge found")


async def _position_amounts(pool_contract) -> tuple[int, int, int, int]:
    slot0 = await pool_contract.functions.slot0().call(block_identifier="latest")
    token0 = await pool_contract.functions.token0().call(block_identifier="latest")
    tick_spacing = await pool_contract.functions.tickSpacing().call(
        block_identifier="latest"
    )
    tick_current = int(slot0[1])
    spacing = int(tick_spacing)
    tick_lower = _round_tick_down(tick_current, spacing) - (10 * spacing)
    tick_upper = _round_tick_down(tick_current, spacing) + (10 * spacing)
    if tick_upper <= tick_lower:
        tick_upper = tick_lower + spacing

    sqrt_price_x96 = int(slot0[0])
    price_token1_per_token0 = (
        Decimal(sqrt_price_x96) * Decimal(sqrt_price_x96) / Decimal(2**192)
    )

    weth_amount = 10**16  # 0.01 WETH
    if token0.lower() == WETH.lower():
        amount0 = weth_amount
        amount1 = max(
            10**18,
            int((Decimal(amount0) * price_token1_per_token0) * Decimal("1.20")),
        )
    else:
        amount1 = weth_amount
        amount0 = max(
            10**18,
            int((Decimal(amount1) / price_token1_per_token0) * Decimal("1.20")),
        )

    return amount0, amount1, tick_lower, tick_upper


@pytest.mark.asyncio
async def test_gorlami_aerodrome_slipstream_position_lifecycle(gorlami):
    fork_id = await _ensure_fork(gorlami)

    acct = Account.create()
    adapter = _make_adapter(acct)

    await gorlami.set_native_balance(fork_id, acct.address, 10 * 10**18)
    await gorlami.set_erc20_balance(fork_id, AERO, acct.address, 100_000 * 10**18)
    await gorlami.set_erc20_balance(fork_id, WETH, acct.address, 10 * 10**18)

    match, gauge = await _discover_live_market(adapter)
    deployment_variant = str(match["deployment_variant"])
    pool = str(match["pool"])

    ok, reward_contracts = await adapter.get_reward_contracts(gauge=gauge)
    assert ok is True, reward_contracts
    assert reward_contracts["fees"].startswith("0x")
    assert reward_contracts["bribes"].startswith("0x")

    async with web3_utils.web3_from_chain_id(CHAIN_ID) as web3:
        pool_contract = web3.eth.contract(
            address=web3.to_checksum_address(pool),
            abi=AERODROME_SLIPSTREAM_CL_POOL_ABI,
        )
        gauge_contract = web3.eth.contract(
            address=web3.to_checksum_address(gauge),
            abi=AERODROME_SLIPSTREAM_CL_GAUGE_ABI,
        )
        npm_address = await gauge_contract.functions.nft().call(
            block_identifier="latest"
        )
        npm = web3.eth.contract(
            address=web3.to_checksum_address(npm_address),
            abi=AERODROME_SLIPSTREAM_NPM_ABI,
        )
        token0 = await pool_contract.functions.token0().call(block_identifier="latest")
        token1 = await pool_contract.functions.token1().call(block_identifier="latest")
        tick_spacing = await pool_contract.functions.tickSpacing().call(
            block_identifier="latest"
        )
        amount0, amount1, tick_lower, tick_upper = await _position_amounts(
            pool_contract
        )

    ok, minted = await adapter.mint_position(
        token0=token0,
        token1=token1,
        tick_spacing=int(tick_spacing),
        tick_lower=tick_lower,
        tick_upper=tick_upper,
        amount0_desired=amount0,
        amount1_desired=amount1,
        deployment_variant=deployment_variant,
    )
    assert ok is True, minted
    assert isinstance(minted, dict)
    assert minted["tx"].startswith("0x")
    token_id = minted["token_id"]
    assert token_id is not None

    async with web3_utils.web3_from_chain_id(CHAIN_ID) as web3:
        npm = web3.eth.contract(
            address=web3.to_checksum_address(npm_address),
            abi=AERODROME_SLIPSTREAM_NPM_ABI,
        )
        owner_before_stake = await npm.functions.ownerOf(int(token_id)).call(
            block_identifier="pending"
        )
        pos_before = await npm.functions.positions(int(token_id)).call(
            block_identifier="pending"
        )
    assert owner_before_stake.lower() == acct.address.lower()
    assert int(pos_before[7]) > 0

    ok, pos = await adapter.get_pos(
        token_id=int(token_id), position_manager=npm_address
    )
    assert ok is True, pos
    assert pos["pool"].lower() == pool.lower()
    assert pos["gauge"].lower() == gauge.lower()
    assert int(pos["liquidity"]) > 0
    liquidity = int(pos["liquidity"])

    ok, tx = await adapter.stake_position(gauge=gauge, token_id=int(token_id))
    assert ok is True, tx
    assert isinstance(tx, str) and tx.startswith("0x")

    async with web3_utils.web3_from_chain_id(CHAIN_ID) as web3:
        gauge_contract = web3.eth.contract(
            address=web3.to_checksum_address(gauge),
            abi=AERODROME_SLIPSTREAM_CL_GAUGE_ABI,
        )
        npm = web3.eth.contract(
            address=web3.to_checksum_address(npm_address),
            abi=AERODROME_SLIPSTREAM_NPM_ABI,
        )
        owner_staked = await npm.functions.ownerOf(int(token_id)).call(
            block_identifier="pending"
        )
        staked_contains = await gauge_contract.functions.stakedContains(
            acct.address,
            int(token_id),
        ).call(block_identifier="pending")
    assert owner_staked.lower() == gauge.lower()
    assert staked_contains is True

    ok, tx = await adapter.claim_position_rewards(gauge=gauge, token_id=int(token_id))
    assert ok is True, tx
    assert isinstance(tx, str) and tx.startswith("0x")

    ok, tx = await adapter.unstake_position(gauge=gauge, token_id=int(token_id))
    assert ok is True, tx
    assert isinstance(tx, str) and tx.startswith("0x")

    async with web3_utils.web3_from_chain_id(CHAIN_ID) as web3:
        npm = web3.eth.contract(
            address=web3.to_checksum_address(npm_address),
            abi=AERODROME_SLIPSTREAM_NPM_ABI,
        )
        owner_after_unstake = await npm.functions.ownerOf(int(token_id)).call(
            block_identifier="pending"
        )
    assert owner_after_unstake.lower() == acct.address.lower()

    ok, tx = await adapter.collect_fees(
        token_id=int(token_id),
        position_manager=npm_address,
    )
    assert ok is True, tx

    ok, tx = await adapter.decrease_liquidity(
        token_id=int(token_id),
        position_manager=npm_address,
        liquidity=liquidity,
    )
    assert ok is True, tx

    ok, tx = await adapter.collect_fees(
        token_id=int(token_id),
        position_manager=npm_address,
    )
    assert ok is True, tx

    ok, tx = await adapter.burn_position(
        token_id=int(token_id),
        position_manager=npm_address,
    )
    assert ok is True, tx


@pytest.mark.asyncio
async def test_gorlami_aerodrome_slipstream_vote_and_state(gorlami):
    fork_id = await _ensure_fork(gorlami)

    acct = Account.create()
    adapter = _make_adapter(acct)

    await gorlami.set_native_balance(fork_id, acct.address, 10 * 10**18)
    await gorlami.set_erc20_balance(fork_id, AERO, acct.address, 5_000 * 10**18)

    match, _ = await _discover_live_market(adapter)
    pool = str(match["pool"])

    ok, res = await adapter.create_lock(
        amount=25 * 10**18,
        lock_duration=4 * WEEK_SECONDS,
    )
    assert ok is True, res
    token_id = int(res["token_id"])

    advanced = await _move_to_safe_vote_window(gorlami, fork_id)
    if not advanced:
        pytest.skip(
            "Gorlami fork backend does not support Aerodrome vote-window time travel"
        )

    ok, tx = await adapter.vote(token_id=token_id, pools=[pool], weights=[10_000])
    assert ok is True, tx
    assert isinstance(tx, str) and tx.startswith("0x")

    async with web3_utils.web3_from_chain_id(CHAIN_ID) as web3:
        voter = web3.eth.contract(
            address=web3.to_checksum_address(str(ENTRY["voter"])),
            abi=AERODROME_VOTER_ABI,
        )
        ve = web3.eth.contract(
            address=web3.to_checksum_address(str(ENTRY["voting_escrow"])),
            abi=AERODROME_VOTING_ESCROW_ABI,
        )
        vote_weight = await voter.functions.votes(token_id, pool).call(
            block_identifier="pending"
        )
        voted_flag = await ve.functions.voted(token_id).call(block_identifier="pending")

    assert int(vote_weight) > 0
    assert voted_flag is True

    ok, token_ids = await adapter.get_user_ve_nfts(owner=acct.address)
    assert ok is True, token_ids
    assert int(token_id) in [int(x) for x in token_ids]

    advanced = await _move_to_next_safe_vote_window(gorlami, fork_id)
    if not advanced:
        pytest.skip(
            "Gorlami fork backend does not support Aerodrome next-epoch time travel"
        )

    ok, tx = await adapter.reset_vote(token_id=token_id)
    assert ok is True, tx
    assert isinstance(tx, str) and tx.startswith("0x")

    async with web3_utils.web3_from_chain_id(CHAIN_ID) as web3:
        voter = web3.eth.contract(
            address=web3.to_checksum_address(str(ENTRY["voter"])),
            abi=AERODROME_VOTER_ABI,
        )
        ve = web3.eth.contract(
            address=web3.to_checksum_address(str(ENTRY["voting_escrow"])),
            abi=AERODROME_VOTING_ESCROW_ABI,
        )
        vote_weight_after = await voter.functions.votes(token_id, pool).call(
            block_identifier="pending"
        )
        voted_flag_after = await ve.functions.voted(token_id).call(
            block_identifier="pending"
        )

    assert int(vote_weight_after) == 0
    assert voted_flag_after is False


EPOCH_SPECIAL_WINDOW_SECONDS = aerodrome_common_module.EPOCH_SPECIAL_WINDOW_SECONDS
WEEK_SECONDS = aerodrome_common_module.WEEK_SECONDS
