import construct
import mango
import mango.marketmaking
import typing

from decimal import Decimal
from pyserum.market.market import Market as PySerumMarket
from pyserum.market.state import MarketState as PySerumMarketState
from solana.keypair import Keypair
from solana.publickey import PublicKey
from solana.rpc.api import Client
from solana.rpc.commitment import Commitment
from solana.rpc.types import RPCResponse


class MockCompatibleClient(Client):
    def __init__(self) -> None:
        super().__init__("http://localhost", Commitment("processed"))
        self.token_accounts_by_owner: typing.Sequence[typing.Any] = []

    def get_token_accounts_by_owner(
        self, *args: typing.Any, **kwargs: typing.Any
    ) -> RPCResponse:
        return RPCResponse(result={"value": self.token_accounts_by_owner})

    def get_minimum_balance_for_rent_exemption(
        size, *args: typing.Any, **kwargs: typing.Any
    ) -> RPCResponse:
        return RPCResponse(result=27)


class MockClient(mango.BetterClient):
    def __init__(self) -> None:
        rpc = mango.RPCCaller(
            "fake",
            "http://localhost",
            "ws://localhost",
            -1,
            [],
            mango.NullSlotHolder(),
            mango.InstructionReporter(),
        )
        compound = mango.CompoundRPCCaller("fake", [rpc])
        super().__init__(
            MockCompatibleClient(),
            "test",
            "local",
            Commitment("processed"),
            False,
            -1,
            "base64",
            0,
            compound,
        )


def fake_public_key() -> PublicKey:
    return PublicKey("11111111111111111111111111111112")


def fake_seeded_public_key(seed: str) -> PublicKey:
    return PublicKey.create_with_seed(
        PublicKey("11111111111111111111111111111112"),
        seed,
        PublicKey("11111111111111111111111111111111"),
    )


def fake_context(
    mango_program_address: typing.Optional[PublicKey] = None,
) -> mango.Context:
    context = mango.Context(
        name="Mango Test",
        cluster_name="test",
        cluster_urls=[
            mango.ClusterUrlData(rpc="http://localhost"),
            mango.ClusterUrlData(rpc="http://localhost"),
        ],
        skip_preflight=False,
        tpu_retransmissions=-1,
        commitment="processed",
        encoding="base64",
        blockhash_cache_duration=0,
        http_request_timeout=-1,
        stale_data_pauses_before_retry=[],
        mango_program_address=mango_program_address
        or fake_seeded_public_key("Mango program address"),
        serum_program_address=fake_seeded_public_key("Serum program address"),
        group_name="TEST_GROUP",
        group_address=fake_seeded_public_key("group ID"),
        gma_chunk_size=Decimal(20),
        gma_chunk_pause=Decimal(25),
        reflink=None,
        instrument_lookup=mango.IdsJsonTokenLookup("devnet", "devnet.2"),
        market_lookup=mango.NullMarketLookup(),
    )
    context.client = MockClient()
    return context


def fake_account_info(
    address: typing.Optional[PublicKey] = None,
    executable: bool = False,
    lamports: Decimal = Decimal(0),
    owner: PublicKey = fake_public_key(),
    rent_epoch: Decimal = Decimal(0),
    data: bytes = bytes([0]),
) -> mango.AccountInfo:
    if address is None:
        address = fake_public_key()
    return mango.AccountInfo(address, executable, lamports, owner, rent_epoch, data)


def fake_instrument(symbol: str = "FAKE", decimals: int = 6) -> mango.Instrument:
    return mango.Instrument(symbol, f"Fake Instrument ({symbol})", Decimal(decimals))


def fake_token(symbol: str = "FAKE", decimals: int = 6) -> mango.Token:
    return mango.Token(
        symbol,
        f"Fake Token ({symbol})",
        Decimal(decimals),
        fake_seeded_public_key(f"fake token ({symbol})"),
    )


def fake_perp_account() -> mango.PerpAccount:
    return mango.PerpAccount(
        Decimal(0),
        Decimal(0),
        Decimal(0),
        Decimal(0),
        Decimal(0),
        Decimal(0),
        Decimal(0),
        Decimal(0),
        fake_instrument_value(),
        mango.PerpOpenOrders([]),
        mango.NullLotSizeConverter(),
        fake_instrument_value(),
        Decimal(0),
        Decimal(0),
    )


def fake_token_bank(symbol: str = "FAKE") -> mango.TokenBank:
    return mango.TokenBank(fake_token(symbol), fake_seeded_public_key("root bank"))


def fake_market() -> PySerumMarket:
    # Container = NamedTuple("Container", [("own_address", PublicKey), ("vault_signer_nonce", int)])
    container = construct.Container(
        {
            "own_address": fake_seeded_public_key("market address"),
            "vault_signer_nonce": 2,
        }
    )
    # container: Container[typing.Any] = Container(
    #     own_address=fake_seeded_public_key("market address"), vault_signer_nonce=2)
    state = PySerumMarketState(container, fake_seeded_public_key("program ID"), 6, 6)
    state.base_vault = lambda: fake_seeded_public_key("base vault")  # type: ignore[assignment]
    state.quote_vault = lambda: fake_seeded_public_key("quote vault")  # type: ignore[assignment]
    state.event_queue = lambda: fake_seeded_public_key("event queue")  # type: ignore[assignment]
    state.request_queue = lambda: fake_seeded_public_key("request queue")  # type: ignore[assignment]
    state.bids = lambda: fake_seeded_public_key("bids")  # type: ignore[assignment]
    state.asks = lambda: fake_seeded_public_key("asks")  # type: ignore[assignment]
    state.base_lot_size = lambda: 1  # type: ignore[assignment]
    state.quote_lot_size = lambda: 1  # type: ignore[assignment]
    return PySerumMarket(MockCompatibleClient(), state)


def fake_spot_market_stub() -> mango.SpotMarketStub:
    return mango.SpotMarketStub(
        fake_seeded_public_key("program ID"),
        fake_seeded_public_key("spot market"),
        fake_token("BASE"),
        fake_token("QUOTE"),
        fake_seeded_public_key("group address"),
    )


def fake_loaded_market(
    base_lot_size: Decimal = Decimal(1), quote_lot_size: Decimal = Decimal(1)
) -> mango.LoadedMarket:
    class FakeLoadedMarket(mango.LoadedMarket):
        @property
        def fully_qualified_symbol(self) -> str:
            return "full:MARKET/SYMBOL"

        @property
        def bids_address(self) -> PublicKey:
            return fake_seeded_public_key("bids_address")

        @property
        def asks_address(self) -> PublicKey:
            return fake_seeded_public_key("asks_address")

        @property
        def event_queue_address(self) -> PublicKey:
            return fake_seeded_public_key("event_queue_address")

        def on_fill(
            self,
            context: mango.Context,
            handler: typing.Callable[[mango.FillEvent], None],
        ) -> mango.Disposable:
            return mango.Disposable()

        def on_event(
            self, context: mango.Context, handler: typing.Callable[[mango.Event], None]
        ) -> mango.Disposable:
            return mango.Disposable()

        def parse_account_info_to_orders(
            self, account_info: mango.AccountInfo
        ) -> typing.Sequence[mango.Order]:
            return []

    base = fake_token("BASE")
    quote = fake_token("QUOTE")
    return FakeLoadedMarket(
        mango.MarketType.PERP,
        fake_seeded_public_key("program ID"),
        fake_seeded_public_key("perp market"),
        mango.InventorySource.ACCOUNT,
        base,
        quote,
        mango.LotSizeConverter(base, base_lot_size, quote, quote_lot_size),
    )


def fake_token_account() -> mango.TokenAccount:
    token_account_info = fake_account_info()
    token = fake_token()
    token_value = mango.InstrumentValue(token, Decimal("100"))
    return mango.TokenAccount(
        token_account_info,
        mango.Version.V1,
        fake_seeded_public_key("owner"),
        token_value,
    )


def fake_instrument_value(value: Decimal = Decimal(100)) -> mango.InstrumentValue:
    return mango.InstrumentValue(fake_token(), value)


def fake_wallet() -> mango.Wallet:
    wallet = mango.Wallet(bytes([1] * 64))
    wallet.keypair = Keypair()
    return wallet


def fake_order(
    price: Decimal = Decimal(1),
    quantity: Decimal = Decimal(1),
    side: mango.Side = mango.Side.BUY,
    order_type: mango.OrderType = mango.OrderType.LIMIT,
) -> mango.Order:
    return mango.Order.from_values(
        side=side, price=price, quantity=quantity, order_type=order_type
    )


# serum ID structure - 16-byte 'int': low 8 bytes is a sequence number, high 8 bytes is price
def fake_order_id(index: int, price: int) -> int:
    # price needs to be max of 64bit/8bytes, considering signed int is not permitted
    if index > (2**64) - 1 or price > (2**64) - 1:
        raise ValueError(
            f"Provided index '{index}' or price '{price}' is bigger than 8 bytes int"
        )
    index_bytes = index.to_bytes(8, byteorder="big", signed=False)
    price_bytes = price.to_bytes(8, byteorder="big", signed=False)
    return int.from_bytes((price_bytes + index_bytes), byteorder="big", signed=False)


def fake_price(
    market: mango.LoadedMarket = fake_loaded_market(),
    price: Decimal = Decimal(100),
    bid: Decimal = Decimal(99),
    ask: Decimal = Decimal(101),
) -> mango.Price:
    return mango.Price(
        mango.OracleSource(
            "test", "test", mango.SupportedOracleFeature.TOP_BID_AND_OFFER, market
        ),
        mango.utc_now(),
        market,
        bid,
        price,
        ask,
        Decimal(0),
    )


def fake_placed_orders_container() -> mango.PlacedOrdersContainer:
    return mango.PerpOpenOrders([])


def fake_inventory(
    incentives: Decimal = Decimal(1),
    available: Decimal = Decimal(100),
    base: Decimal = Decimal(10),
    quote: Decimal = Decimal(10),
) -> mango.Inventory:
    return mango.Inventory(
        mango.InventorySource.SPL_TOKENS,
        fake_instrument_value(incentives),
        fake_instrument_value(available),
        fake_instrument_value(base),
        fake_instrument_value(quote),
    )


def fake_bids() -> typing.Sequence[mango.Order]:
    return []


def fake_asks() -> typing.Sequence[mango.Order]:
    return []


def fake_account_slot() -> mango.AccountSlot:
    return mango.AccountSlot(
        1,
        fake_instrument(),
        fake_token_bank(),
        fake_token_bank(),
        Decimal(1),
        fake_instrument_value(),
        Decimal(0),
        fake_instrument_value(),
        fake_seeded_public_key("open_orders"),
        None,
    )


def fake_account(address: typing.Optional[PublicKey] = None) -> mango.Account:
    meta_data = mango.Metadata(mango.layouts.DATA_TYPE.Account, mango.Version.V1, True)
    quote = fake_account_slot()
    return mango.Account(
        fake_account_info(address=address),
        mango.Version.V1,
        meta_data,
        "GROUPNAME",
        fake_seeded_public_key("group"),
        fake_seeded_public_key("owner"),
        "INFO",
        quote,
        [],
        [],
        [],
        Decimal(1),
        False,
        False,
        fake_seeded_public_key("advanced_orders"),
        False,
        fake_seeded_public_key("delegate"),
    )


def fake_root_bank() -> mango.RootBank:
    meta_data = mango.Metadata(mango.layouts.DATA_TYPE.RootBank, mango.Version.V1, True)
    return mango.RootBank(
        fake_account_info(),
        mango.Version.V1,
        meta_data,
        Decimal(0),
        Decimal(0),
        Decimal(0),
        [],
        Decimal(0),
        Decimal(0),
        mango.utc_now(),
    )


def fake_cache() -> mango.Cache:
    meta_data = mango.Metadata(mango.layouts.DATA_TYPE.RootBank, mango.Version.V1, True)
    return mango.Cache(fake_account_info(), mango.Version.V1, meta_data, [], [], [])


def fake_root_bank_cache() -> mango.RootBankCache:
    return mango.RootBankCache(
        Decimal(1),
        Decimal(2),
        mango.utc_now(),
    )


def fake_group(address: typing.Optional[PublicKey] = None) -> mango.Group:
    account_info = fake_account_info(address=address)
    name = "FAKE_GROUP"
    meta_data = mango.Metadata(mango.layouts.DATA_TYPE.Group, mango.Version.V1, True)
    instrument_lookup = fake_context().instrument_lookup
    usdc = mango.Token.ensure(instrument_lookup.find_by_symbol_or_raise("usdc"))
    quote_info = mango.TokenBank(usdc, fake_seeded_public_key("root bank"))
    signer_nonce = Decimal(1)
    signer_key = fake_seeded_public_key("signer key")
    admin_key = fake_seeded_public_key("admin key")
    serum_program_address = fake_seeded_public_key("DEX program ID")
    cache_key = fake_seeded_public_key("cache key")
    valid_interval = Decimal(7)
    insurance_vault = fake_seeded_public_key("insurance vault")
    srm_vault = fake_seeded_public_key("srm vault")
    msrm_vault = fake_seeded_public_key("msrm vault")
    fees_vault = fake_seeded_public_key("fees vault")
    max_mango_accounts = Decimal(1000000)
    num_mango_accounts = Decimal(1)
    referral_surcharge_centibps = Decimal(7)
    referral_share_centibps = Decimal(8)
    referral_mngo_required = Decimal(9)

    return mango.Group(
        account_info,
        mango.Version.V1,
        name,
        meta_data,
        quote_info,
        [],
        [],
        signer_nonce,
        signer_key,
        admin_key,
        serum_program_address,
        cache_key,
        valid_interval,
        insurance_vault,
        srm_vault,
        msrm_vault,
        fees_vault,
        max_mango_accounts,
        num_mango_accounts,
        referral_surcharge_centibps,
        referral_share_centibps,
        referral_mngo_required,
    )


def fake_prices(prices: typing.Sequence[str]) -> typing.Sequence[mango.InstrumentValue]:
    instrument_lookup = fake_context().instrument_lookup
    ETH = mango.Token.ensure(instrument_lookup.find_by_symbol_or_raise("ETH"))
    BTC = mango.Token.ensure(instrument_lookup.find_by_symbol_or_raise("BTC"))
    SOL = mango.Token.ensure(instrument_lookup.find_by_symbol_or_raise("SOL"))
    SRM = mango.Token.ensure(instrument_lookup.find_by_symbol_or_raise("SRM"))
    USDC = mango.Token.ensure(instrument_lookup.find_by_symbol_or_raise("USDC"))
    eth, btc, sol, srm, usdc = prices

    return [
        mango.InstrumentValue(ETH, Decimal(eth)),
        mango.InstrumentValue(BTC, Decimal(btc)),
        mango.InstrumentValue(SOL, Decimal(sol)),
        mango.InstrumentValue(SRM, Decimal(srm)),
        mango.InstrumentValue(USDC, Decimal(usdc)),
    ]


def fake_open_orders(
    base_token_free: Decimal = Decimal(0),
    base_token_total: Decimal = Decimal(0),
    quote_token_free: Decimal = Decimal(0),
    quote_token_total: Decimal = Decimal(0),
    referrer_rebate_accrued: Decimal = Decimal(0),
) -> mango.OpenOrders:
    account_info = fake_account_info()
    program_address = fake_seeded_public_key("program address")
    market = fake_seeded_public_key("market")
    owner = fake_seeded_public_key("owner")

    base = fake_token("FAKEBASE")
    quote = fake_token("FAKEQUOTE")
    flags = mango.AccountFlags(
        mango.Version.V1, True, False, True, False, False, False, False, False
    )
    return mango.OpenOrders(
        account_info,
        mango.Version.V1,
        program_address,
        flags,
        market,
        owner,
        base,
        quote,
        base_token_free,
        base_token_total,
        quote_token_free,
        quote_token_total,
        [],
        referrer_rebate_accrued,
    )


def fake_model_state(
    order_owner: typing.Optional[PublicKey] = None,
    market: typing.Optional[mango.Market] = None,
    group: typing.Optional[mango.Group] = None,
    account: typing.Optional[mango.Account] = None,
    price: typing.Optional[mango.Price] = None,
    placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None,
    inventory: typing.Optional[mango.Inventory] = None,
    orderbook: typing.Optional[mango.OrderBook] = None,
    event_queue: typing.Optional[mango.EventQueue] = None,
) -> mango.ModelState:
    order_owner = order_owner or fake_seeded_public_key("order owner")
    market = market or fake_loaded_market()
    group = group or fake_group()
    account = account or fake_account()
    price = price or fake_price()
    placed_orders_container = placed_orders_container or fake_placed_orders_container()
    inventory = inventory or fake_inventory()
    orderbook = orderbook or mango.OrderBook(
        "FAKE", mango.NullLotSizeConverter(), fake_bids(), fake_asks()
    )
    event_queue = event_queue or mango.NullEventQueue()
    group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(
        group
    )
    account_watcher: mango.ManualUpdateWatcher[
        mango.Account
    ] = mango.ManualUpdateWatcher(account)
    price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(
        price
    )
    placed_orders_container_watcher: mango.ManualUpdateWatcher[
        mango.PlacedOrdersContainer
    ] = mango.ManualUpdateWatcher(placed_orders_container)
    inventory_watcher: mango.ManualUpdateWatcher[
        mango.Inventory
    ] = mango.ManualUpdateWatcher(inventory)
    orderbook_watcher: mango.ManualUpdateWatcher[
        mango.OrderBook
    ] = mango.ManualUpdateWatcher(orderbook)
    event_queue_watcher: mango.ManualUpdateWatcher[
        mango.EventQueue
    ] = mango.ManualUpdateWatcher(event_queue)

    return mango.ModelState(
        order_owner,
        market,
        group_watcher,
        account_watcher,
        price_watcher,
        placed_orders_container_watcher,
        inventory_watcher,
        orderbook_watcher,
        event_queue_watcher,
    )


def fake_mango_instruction() -> mango.MangoInstruction:
    return mango.MangoInstruction(
        fake_seeded_public_key("program id"),
        mango.InstructionType.PlacePerpOrder,
        bytes(),
        "",
        [fake_seeded_public_key("account")],
    )
