"""
A login menu using EvMenu.

Contribution - Vincent-lg 2016, Griatch 2019 (rework for modern EvMenu)

This changes the Evennia login to ask for the account name and password in
sequence instead of requiring you to enter both at once.

To install, add these lines to the settings file (`mygame/server/conf/settings.py`):

    CMDSET_UNLOGGEDIN = "evennia.contrib.base_systems.menu_login.UnloggedinCmdSet"
    CONNECTION_SCREEN_MODULE = "evennia.contrib.base_systems.menu_login.connection_screens"

Reload the server and the new connection method will be active. If you want to modify the
way the connection screen looks, use the current one as a guide and create a new one in your
game folder. Then update the settings file CONNECTION_SCREEN_MODULE to point to yours.

This uses Evennia's menu system EvMenu and is triggered by a command that is
called automatically when a new user connects.

"""

from django.conf import settings

from evennia import CmdSet, Command, syscmdkeys
from evennia.utils.evmenu import EvMenu
from evennia.utils.utils import (
    callables_from_module,
    class_from_module,
    random_string_from_module,
)

_CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
_GUEST_ENABLED = settings.GUEST_ENABLED
_ACCOUNT = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
_GUEST = class_from_module(settings.BASE_GUEST_TYPECLASS)

_ACCOUNT_HELP = "Enter a new or existing login name."
_PASSWORD_HELP = (
    "Password should be a minimum of 8 characters (preferably longer) and "
    "can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."
)

# Menu nodes


def _show_help(caller, raw_string, **kwargs):
    """Echo help message, then re-run node that triggered it"""
    help_entry = kwargs["help_entry"]
    caller.msg(help_entry)
    return None  # re-run calling node


def node_enter_username(caller, raw_text, **kwargs):
    """
    Start node of menu
    Start login by displaying the connection screen and ask for a user name.

    """

    def _check_input(caller, username, **kwargs):
        """
        'Goto-callable', set up to be called from the _default option below.

        Called when user enters a username string. Check if this username already exists and set the
        flag 'new_user' if not. Will also directly login if the username is 'guest' and
        GUEST_ENABLED is True.

        The return from this goto-callable determines which node we go to next
        and what kwarg it will be called with.

        """
        username = username.rstrip("\n")

        if username == "guest" and _GUEST_ENABLED:
            # do an immediate guest login
            session = caller
            address = session.address
            account, errors = _GUEST.authenticate(ip=address)
            if account:
                return "node_quit_or_login", {"login": True, "account": account}
            else:
                session.msg("|R{}|n".format("\n".join(errors)))
                return None  # re-run the username node

        try:
            _ACCOUNT.objects.get(username__iexact=username)
        except _ACCOUNT.DoesNotExist:
            new_user = True
        else:
            new_user = False

        if new_user and not settings.ACCOUNT_REGISTRATION_ENABLED:
            caller.msg("Registration is currently disabled.")
            return None

        # pass username/new_user into next node as kwargs
        return "node_enter_password", {"new_user": new_user, "username": username}

    callables = callables_from_module(_CONNECTION_SCREEN_MODULE)
    if "connection_screen" in callables:
        connection_screen = callables["connection_screen"]()
    else:
        connection_screen = random_string_from_module(_CONNECTION_SCREEN_MODULE)

    if _GUEST_ENABLED:
        text = "Enter a new or existing user name to login (write 'guest' for a guest login):"
    else:
        text = "Enter a new or existing user name to login:"
    text = "{}\n\n{}".format(connection_screen, text)

    options = (
        {"key": "", "goto": "node_enter_username"},
        {"key": ("quit", "q"), "goto": "node_quit_or_login"},
        {"key": ("help", "h"), "goto": (_show_help, {"help_entry": _ACCOUNT_HELP, **kwargs})},
        {"key": "_default", "goto": _check_input},
    )
    return text, options


def node_enter_password(caller, raw_string, **kwargs):
    """
    Handle password input.

    """

    def _check_input(caller, password, **kwargs):
        """
        'Goto-callable', set up to be called from the _default option below.

        Called when user enters a password string. Check username + password
        viability. If it passes, the account will have been created and login
        will be initiated.

        The return from this goto-callable determines which node we go to next
        and what kwarg it will be called with.

        """
        # these flags were set by the goto-callable
        username = kwargs["username"]
        new_user = kwargs["new_user"]
        password = password.rstrip("\n")

        session = caller
        address = session.address
        if new_user:
            # create a new account
            account, errors = _ACCOUNT.create(
                username=username, password=password, ip=address, session=session
            )
        else:
            # check password against existing account
            account, errors = _ACCOUNT.authenticate(
                username=username, password=password, ip=address, session=session
            )

        if account:
            if new_user:
                session.msg("|gA new account |c{}|g was created. Welcome!|n".format(username))
            # pass login info to login node
            return "node_quit_or_login", {"login": True, "account": account}
        else:
            # restart due to errors
            session.msg("|R{}".format("\n".join(errors)))
            kwargs["retry_password"] = True
            return "node_enter_password", kwargs

    def _restart_login(caller, *args, **kwargs):
        caller.msg("|yCancelled login.|n")
        return "node_enter_username"

    username = kwargs["username"]
    if kwargs["new_user"]:
        if kwargs.get("retry_password"):
            # Attempting to fix password
            text = "Enter a new password:"
        else:
            text = "Creating a new account |c{}|n. Enter a password (empty to abort):".format(
                username
            )
    else:
        text = "Enter the password for account |c{}|n (empty to abort):".format(username)
    options = (
        {"key": "", "goto": _restart_login},
        {"key": ("quit", "q"), "goto": "node_quit_or_login"},
        {"key": ("help", "h"), "goto": (_show_help, {"help_entry": _PASSWORD_HELP, **kwargs})},
        {"key": "_default", "goto": (_check_input, kwargs)},
    )
    return text, options


def node_quit_or_login(caller, raw_text, **kwargs):
    """
    Exit menu, either by disconnecting or logging in.

    """
    session = caller
    if kwargs.get("login"):
        account = kwargs.get("account")
        session.msg("|gLogging in ...|n")
        session.sessionhandler.login(session, account)
    else:
        session.sessionhandler.disconnect(session, "Goodbye! Logging off.")
    return "", {}


# EvMenu helper function


class MenuLoginEvMenu(EvMenu):
    """
    Version of EvMenu that does not display any of its options.

    """

    def node_formatter(self, nodetext, optionstext):
        return nodetext

    def options_formatter(self, optionlist):
        """Do not display the options, only the text.

        This function is used by EvMenu to format the text of nodes. The menu login
        is just a series of prompts so we disable all automatic display decoration
        and let the nodes handle everything on their own.

        """
        return ""


# Commands and CmdSets


class UnloggedinCmdSet(CmdSet):
    "Cmdset for the unloggedin state"
    key = "DefaultUnloggedin"
    priority = 0

    def at_cmdset_creation(self):
        "Called when cmdset is first created."
        self.add(CmdUnloggedinLook())


class CmdUnloggedinLook(Command):
    """
    An unloggedin version of the look command. This is called by the server
    when the account first connects. It sets up the menu before handing off
    to the menu's own look command.

    """

    key = syscmdkeys.CMD_LOGINSTART
    locks = "cmd:all()"
    arg_regex = r"^$"

    def func(self):
        """
        Run the menu using the nodes in this module.

        """
        menu_nodes = {
            "node_enter_username": node_enter_username,
            "node_enter_password": node_enter_password,
            "node_quit_or_login": node_quit_or_login,
        }

        MenuLoginEvMenu(
            self.caller,
            menu_nodes,
            startnode="node_enter_username",
            auto_look=False,
            auto_quit=False,
            cmd_on_exit=None,
        )
