from collections.abc import Sequence
from fractions import Fraction
from typing import Literal

import eth_abi.abi
import eth_abi.packed
from eth_typing import ChecksumAddress
from eth_utils.crypto import keccak
from hexbytes import HexBytes

from degenbot.exceptions.evm import EVMRevertError
from degenbot.functions import eip_1167_clone_address, raise_if_invalid_uint256
from degenbot.solidly.solidly_functions import (
    general_calc_d,
    general_calc_exact_in_stable,
    general_calc_k,
)


def _f_aerodrome(
    x0: int,
    y: int,
) -> int:
    _a = (x0 * y) // 10**18
    _b = (x0 * x0) // 10**18 + (y * y) // 10**18
    return (_a * _b) // 10**18


def calc_exact_in_stable(
    amount_in: int,
    token_in: Literal[0, 1],
    reserves0: int,
    reserves1: int,
    decimals0: int,
    decimals1: int,
    fee: Fraction,
) -> int:
    return general_calc_exact_in_stable(
        amount_in=amount_in,
        token_in=token_in,
        reserves0=reserves0,
        reserves1=reserves1,
        decimals0=decimals0,
        decimals1=decimals1,
        fee=fee,
        k_func=general_calc_k,
        get_y_func=_get_y_aerodrome,
    )


def _get_y_aerodrome(
    x0: int,
    xy: int,
    y: int,
    decimals0: int,
    decimals1: int,
) -> int:  # pragma: no cover
    """
    Calculate the minimum reserves for the withdrawn token that satisfy the pool invariant.

    Reference: https://github.com/aerodrome-finance/contracts/blob/main/contracts/Pool.sol
    """

    for _ in range(255):
        k = _f_aerodrome(x0, y)
        if k < xy:
            dy = ((xy - k) * 10**18) // general_calc_d(x0, y)
            if dy == 0:
                if k == xy:
                    return y
                if (
                    _k_aerodrome(
                        balance_0=x0, balance_1=y + 1, decimals_0=decimals0, decimals_1=decimals1
                    )
                    > xy
                ):
                    return y + 1
                dy = 1
            y = y + dy
        else:
            dy = ((k - xy) * 10**18) // general_calc_d(x0, y)
            if dy == 0:
                if k == xy or _f_aerodrome(x0, y - 1) < xy:
                    return y
                dy = 1
            y = y - dy
    raise EVMRevertError(error="Failed to converge on a value for y")


def _k_aerodrome(
    balance_0: int,
    balance_1: int,
    decimals_0: int,
    decimals_1: int,
) -> int:
    _x = balance_0 * 10**18 // decimals_0
    _y = balance_1 * 10**18 // decimals_1
    _a = (_x * _y) // 10**18
    _b = (_x * _x) // 10**18 + (_y * _y) // 10**18
    raise_if_invalid_uint256(_a * _b)
    return _a * _b // 10**18  # x^3*y + y^3*x >= k


def generate_aerodrome_v2_pool_address(
    *,
    deployer_address: str | bytes,
    token_addresses: Sequence[str | bytes],
    implementation_address: str | bytes,
    stable: bool,
) -> ChecksumAddress:
    """
    Get the deterministic V2 pool address generated by CREATE2. Uses the token address to generate
    the salt. The token addresses can be passed in any order.

    Adapted from https://github.com/aerodrome-finance/contracts/blob/main/contracts/factories/PoolFactory.sol
    and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Clones.sol
    """

    sorted_token_addresses = sorted([HexBytes(address) for address in token_addresses])

    salt = keccak(
        eth_abi.packed.encode_packed(
            ("address", "address", "bool"),
            [*sorted_token_addresses, stable],
        )
    )

    return eip_1167_clone_address(
        deployer=deployer_address,
        implementation_contract=implementation_address,
        salt=salt,
    )


def generate_aerodrome_v3_pool_address(
    deployer_address: str | bytes,
    token_addresses: Sequence[str | bytes],
    implementation_address: str | bytes,
    tick_spacing: int,
) -> ChecksumAddress:
    """
    Get the deterministic V3 pool address generated by CREATE2. Uses the token address to generate
    the salt. The token addresses can be passed in any order.

    Adapted from https://github.com/aerodrome-finance/slipstream/blob/main/contracts/core/CLFactory.sol
    and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Clones.sol
    """

    sorted_token_addresses = sorted([HexBytes(address) for address in token_addresses])

    salt = keccak(
        eth_abi.abi.encode(
            ("address", "address", "int24"),
            [*sorted_token_addresses, tick_spacing],
        )
    )

    return eip_1167_clone_address(
        deployer=deployer_address,
        implementation_contract=implementation_address,
        salt=salt,
    )
