# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
#   [Docs](https://docs.mango.markets/)
#   [Discord](https://discord.gg/67jySBhxrg)
#   [Twitter](https://twitter.com/mangomarkets)
#   [Github](https://github.com/blockworks-foundation)
#   [Email](mailto:hello@blockworks.foundation)


import json
import typing

from decimal import Decimal
from solana.publickey import PublicKey

from .markets import Market
from .marketlookup import MarketLookup
from .serummarket import SerumMarketStub
from .tokens import Instrument, Token


# # 🥭 SerumMarketLookup class
#
# This class allows us to look up Serum market addresses and tokens, all from our Solana
# static data.
#
# The static data is the [Solana token list](https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json) provided by Serum.
#
# You can load a `SerumMarketLookup` class by something like:
# ```
# with open("solana.tokenlist.json") as json_file:
#     token_data = json.load(json_file)
#     spot_market_lookup = SerumMarketLookup(token_data)
# ```
# This uses the same data file as `SPLTokenLookup` but it looks a lot more complicated. The
# main reason for this is that tokens are described in a list, whereas markets are optional
# child attributes of tokens.
#
# To find a token, we can just go through the list.
#
# To find a market, we need to split the market symbol into the two token symbols, go through
# the list, check if the item has the optional `extensions` attribute, and in there see if
# there is a name-value pair for the particular market we're interested in. Also, the
# current file only lists USDC and USDT markets, so that's all we can support this way.
class SerumMarketLookup(MarketLookup):
    def __init__(
        self, serum_program_address: PublicKey, token_data: typing.Dict[str, typing.Any]
    ) -> None:
        super().__init__()
        self.serum_program_address: PublicKey = serum_program_address
        self.token_data: typing.Dict[str, typing.Any] = token_data

    @staticmethod
    def load(
        serum_program_address: PublicKey, token_data_filename: str
    ) -> "SerumMarketLookup":
        with open(token_data_filename, encoding="utf-8") as json_file:
            token_data = json.load(json_file)
            return SerumMarketLookup(serum_program_address, token_data)

    @staticmethod
    def _find_data_by_symbol(
        symbol: str, token_data: typing.Dict[str, typing.Any]
    ) -> typing.Optional[typing.Dict[str, typing.Any]]:
        for token in token_data["tokens"]:
            if Instrument.symbols_match(token["symbol"], symbol):
                return typing.cast(typing.Dict[str, typing.Any], token)
        return None

    @staticmethod
    def _find_token_by_symbol_or_error(
        symbol: str, token_data: typing.Dict[str, typing.Any]
    ) -> Token:
        found_token_data = SerumMarketLookup._find_data_by_symbol(symbol, token_data)
        if found_token_data is None:
            raise Exception(f"Could not find data for token symbol '{symbol}'.")

        return Token(
            symbol,
            found_token_data["name"],
            Decimal(found_token_data["decimals"]),
            PublicKey(found_token_data["address"]),
        )

    def find_by_symbol(self, symbol: str) -> typing.Optional[Market]:
        if "/" not in symbol:
            return None

        symbol = symbol.upper()
        if symbol.startswith("SERUM:"):
            symbol = symbol.split(":", 1)[1]

        base_symbol, quote_symbol = symbol.split("/")
        base_data = SerumMarketLookup._find_data_by_symbol(base_symbol, self.token_data)
        if base_data is None:
            return None
        base = Token(
            base_data["symbol"],
            base_data["name"],
            Decimal(base_data["decimals"]),
            PublicKey(base_data["address"]),
        )

        quote_data = SerumMarketLookup._find_data_by_symbol(
            quote_symbol, self.token_data
        )
        if quote_data is None:
            self._logger.warning(
                f"Could not find data for quote token '{quote_symbol}'"
            )
            return None
        quote = Token(
            quote_data["symbol"],
            quote_data["name"],
            Decimal(quote_data["decimals"]),
            PublicKey(quote_data["address"]),
        )

        if "extensions" not in base_data:
            self._logger.warning(f"No markets found for base token '{base.symbol}'.")
            return None

        if quote.symbol == "USDC":
            if "serumV3Usdc" not in base_data["extensions"]:
                self._logger.warning(
                    f"No USDC market found for base token '{base.symbol}'."
                )
                return None

            market_address_string = base_data["extensions"]["serumV3Usdc"]
            market_address = PublicKey(market_address_string)
        elif quote.symbol == "USDT":
            if "serumV3Usdt" not in base_data["extensions"]:
                self._logger.warning(
                    f"No USDT market found for base token '{base.symbol}'."
                )
                return None

            market_address_string = base_data["extensions"]["serumV3Usdt"]
            market_address = PublicKey(market_address_string)
        else:
            self._logger.warning(
                f"Could not find market with quote token '{quote.symbol}'. Only markets based on USDC or USDT are supported."
            )
            return None

        return SerumMarketStub(self.serum_program_address, market_address, base, quote)

    def find_by_address(self, address: PublicKey) -> typing.Optional[Market]:
        address_string: str = str(address)
        for token_data in self.token_data["tokens"]:
            if "extensions" in token_data:
                if "serumV3Usdc" in token_data["extensions"]:
                    if token_data["extensions"]["serumV3Usdc"] == address_string:
                        market_address_string = token_data["extensions"]["serumV3Usdc"]
                        market_address = PublicKey(market_address_string)
                        base = Token(
                            token_data["symbol"],
                            token_data["name"],
                            Decimal(token_data["decimals"]),
                            PublicKey(token_data["address"]),
                        )
                        quote_data = SerumMarketLookup._find_data_by_symbol(
                            "USDC", self.token_data
                        )
                        if quote_data is None:
                            raise Exception(
                                "Could not load token data for USDC (which should always be present)."
                            )
                        quote = Token(
                            quote_data["symbol"],
                            quote_data["name"],
                            Decimal(quote_data["decimals"]),
                            PublicKey(quote_data["address"]),
                        )
                        return SerumMarketStub(
                            self.serum_program_address, market_address, base, quote
                        )
                if "serumV3Usdt" in token_data["extensions"]:
                    if token_data["extensions"]["serumV3Usdt"] == address_string:
                        market_address_string = token_data["extensions"]["serumV3Usdt"]
                        market_address = PublicKey(market_address_string)
                        base = Token(
                            token_data["symbol"],
                            token_data["name"],
                            Decimal(token_data["decimals"]),
                            PublicKey(token_data["address"]),
                        )
                        quote_data = SerumMarketLookup._find_data_by_symbol(
                            "USDT", self.token_data
                        )
                        if quote_data is None:
                            raise Exception(
                                "Could not load token data for USDT (which should always be present)."
                            )
                        quote = Token(
                            quote_data["symbol"],
                            quote_data["name"],
                            Decimal(quote_data["decimals"]),
                            PublicKey(quote_data["address"]),
                        )
                        return SerumMarketStub(
                            self.serum_program_address, market_address, base, quote
                        )
        return None

    def all_markets(self) -> typing.Sequence[Market]:
        usdt = SerumMarketLookup._find_token_by_symbol_or_error("USDT", self.token_data)
        usdc = SerumMarketLookup._find_token_by_symbol_or_error("USDC", self.token_data)

        all_markets: typing.List[SerumMarketStub] = []
        for token_data in self.token_data["tokens"]:
            if "extensions" in token_data:
                if "serumV3Usdc" in token_data["extensions"]:
                    market_address_string = token_data["extensions"]["serumV3Usdc"]
                    market_address = PublicKey(market_address_string)
                    base = Token(
                        token_data["symbol"],
                        token_data["name"],
                        Decimal(token_data["decimals"]),
                        PublicKey(token_data["address"]),
                    )
                    all_markets += [
                        SerumMarketStub(
                            self.serum_program_address, market_address, base, usdc
                        )
                    ]
                if "serumV3Usdt" in token_data["extensions"]:
                    market_address_string = token_data["extensions"]["serumV3Usdt"]
                    market_address = PublicKey(market_address_string)
                    base = Token(
                        token_data["symbol"],
                        token_data["name"],
                        Decimal(token_data["decimals"]),
                        PublicKey(token_data["address"]),
                    )
                    all_markets += [
                        SerumMarketStub(
                            self.serum_program_address, market_address, base, usdt
                        )
                    ]

        return all_markets
