# # ⚠ 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 typing

from solana.publickey import PublicKey

from .account import Account
from .accountinfo import AccountInfo
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .group import Group
from .tokens import Token
from .tokenaccount import TokenAccount
from .wallet import Wallet


# # 🥭 AccountScout
#
# Required Accounts
#
# Mango Markets code expects some accounts to be present, and if they're not present some
# actions can fail.
#
# From [Daffy on Discord](https://discord.com/channels/791995070613159966/820390560085835786/834024719958147073):
#
# > You need an open orders account for each of the spot markets Mango. And you need a
# token account for each of the tokens.
#
# This notebook (and the `AccountScout` class) can be used to check the required accounts
# are present and maybe in future set up all the required accounts.
#
# (There's no reason not to write the code to fix problems and create any missing accounts.
# It just hasn't been done yet.)
#


# # 🥭 ScoutReport class
#
# The `ScoutReport` class is built up by the `AccountScout` to report errors, warnings and
# details pertaining to a user account.
#


class ScoutReport:
    def __init__(self, address: PublicKey):
        self.address = address
        self.errors: typing.List[str] = []
        self.warnings: typing.List[str] = []
        self.details: typing.List[str] = []

    @property
    def has_errors(self) -> bool:
        return len(self.errors) > 0

    @property
    def has_warnings(self) -> bool:
        return len(self.warnings) > 0

    def add_error(self, error: str) -> None:
        self.errors += [error]

    def add_warning(self, warning: str) -> None:
        self.warnings += [warning]

    def add_detail(self, detail: str) -> None:
        self.details += [detail]

    def __str__(self) -> str:
        def _pad(text_list: typing.List[str]) -> str:
            if len(text_list) == 0:
                return "None"
            padding = "\n        "
            return padding.join(
                map(lambda text: text.replace("\n", padding), text_list)
            )

        error_text = _pad(self.errors)
        warning_text = _pad(self.warnings)
        detail_text = _pad(self.details)
        if len(self.errors) > 0 or len(self.warnings) > 0:
            summary = f"Found {len(self.errors)} error(s) and {len(self.warnings)} warning(s)."
        else:
            summary = "No problems found"

        return f"""« ScoutReport [{self.address}]:
    Summary:
        {summary}

    Errors:
        {error_text}

    Warnings:
        {warning_text}

    Details:
        {detail_text}
»"""

    def __repr__(self) -> str:
        return f"{self}"


# # 🥭 AccountScout class
#
# The `AccountScout` class aims to run various checks against a user account to make sure
# it is in a position to run the liquidator.
#
# Passing all checks here with no errors will be a precondition on liquidator startup.
#


class AccountScout:
    def __init__(self) -> None:
        pass

    def require_account_prepared_for_group(
        self, context: Context, group: Group, account_address: PublicKey
    ) -> None:
        report = self.verify_account_prepared_for_group(context, group, account_address)
        if report.has_errors:
            raise Exception(
                f"Account '{account_address}' is not prepared for group '{group.address}':\n\n{report}"
            )

    def verify_account_prepared_for_group(
        self, context: Context, group: Group, account_address: PublicKey
    ) -> ScoutReport:
        report = ScoutReport(account_address)

        # First of all, the account must actually exist. If it doesn't, just return early.
        root_account = AccountInfo.load(context, account_address)
        if root_account is None:
            report.add_error(f"Root account '{account_address}' does not exist.")
            return report

        # For this to be a user/wallet account, it must be owned by 11111111111111111111111111111111.
        if root_account.owner != SYSTEM_PROGRAM_ADDRESS:
            report.add_error(f"Account '{account_address}' is not a root user account.")
            return report

        # Must have token accounts for each of the tokens in the group's basket.
        for basket_token in group.tokens:
            if isinstance(basket_token.token, Token):
                token_accounts = TokenAccount.fetch_all_for_owner_and_token(
                    context, account_address, basket_token.token
                )
                if len(token_accounts) == 0:
                    report.add_error(
                        f"Account '{account_address}' has no account for token '{basket_token.token.name}'."
                    )
                else:
                    report.add_detail(
                        f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s): {[ta.address for ta in token_accounts]}"
                    )

        # May have one or more Mango Markets margin account, but it's optional for liquidating
        accounts = Account.load_all_for_owner(context, account_address, group)
        if len(accounts) == 0:
            report.add_detail(
                f"Account '{account_address}' has no Mango Markets margin accounts."
            )
        else:
            for account in accounts:
                report.add_detail(f"Margin account: {account}")

        return report

    # It would be good to be able to fix an account automatically, which should
    # be possible if a Wallet is passed.
    def prepare_wallet_for_group(self, wallet: Wallet, group: Group) -> ScoutReport:
        report = ScoutReport(wallet.address)
        report.add_error("AccountScout can't yet prepare wallets.")
        return report
