"""
A login menu using EvMenu.

Contribution - Vincent-lg 2016

This module contains the functions (nodes) of the EvMenu, with the
CmdSet and UnloggedCommand called when a user logs in.  In other
words, instead of using the 'connect' or 'create' commands once on the
login screen, players navigates through a simple menu asking them to
enter their username followed by password, or to type 'new' to create
a new one. You will need to change update your login screen if you use
this system.

In order to install, to your settings file, add/edit the line:

CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedinCmdSet"

When you'll reload the server, new sessions will connect to the new
login system, where they will be able to:

* Enter their username, assuming they have an existing account.
* Enter 'NEW' to create a new account.

The top-level functions in this file are menu nodes (as described in
evennia.utils.evmenu.py). Each one of these functions is responsible
for prompting the user for a specific piece of information (username,
password and so on). At the bottom of the file is defined the CmdSet
for Unlogging users. This adds a new command that is called just after
a new session has been created, in order to create the menu.  See the
specific documentation on functions (nodes) to see what each one
should do.

"""

import re
from textwrap import dedent

from django.conf import settings

from evennia import Command, CmdSet
from evennia import logger
from evennia import managers
from evennia import ObjectDB
from evennia.server.models import ServerConfig
from evennia import syscmdkeys
from evennia.utils.evmenu import EvMenu
from evennia.utils.utils import random_string_from_module

# Constants
RE_VALID_USERNAME = re.compile(r"^[a-z]{3,}$", re.I)
LEN_PASSWD = 6
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE

# Menu notes (top-level functions)


def start(caller):
    """The user should enter his/her username or NEW to create one.

    This node is called at the very beginning of the menu, when
    a session has been created OR if an error occurs further
    down the menu tree.  From there, users can either enter a
    username (if this username exists) or type NEW (capitalized
    or not) to create a new account.

    """
    text = random_string_from_module(CONNECTION_SCREEN_MODULE)
    text += "\n\nEnter your username or |yNEW|n to create a new account."
    options = (
        {"key": "",
         "goto": "start"},
        {"key": "new",
         "goto": "create_account"},
        {"key": "quit",
         "goto": "quit"},
        {"key": "_default",
         "goto": "username"})
    return text, options


def username(caller, string_input):
    """Check that the username leads to an existing account.

    Check that the specified username exists.  If the username doesn't
    exist, display an error message and ask the user to try again.  If
    entering an empty string, return to start node.  If user exists,
    move to the next node (enter password).

    """
    string_input = string_input.strip()
    account = managers.accounts.get_account_from_name(string_input)
    if account is None:
        text = dedent("""
            |rThe username '{}' doesn't exist. Have you created it?|n
            Try another name or leave empty to go back.
        """.strip("\n")).format(string_input)
        options = (
            {"key": "",
             "goto": "start"},
            {"key": "_default",
             "goto": "username"})
    else:
        caller.ndb._menutree.account = account
        text = "Enter the password for the {} account.".format(account.name)
        # Disables echo for the password
        caller.msg("", options={"echo": False})
        options = (
            {"key": "",
             "exec": lambda caller: caller.msg("", options={"echo": True}),
             "goto": "start"},
            {"key": "_default",
             "goto": "ask_password"})

    return text, options


def ask_password(caller, string_input):
    """Ask the user to enter the password to this account.

    This is assuming the user exists (see 'create_username' and
    'create_password').  This node "loops" if needed:  if the
    user specifies a wrong password, offers the user to try
    again or to go back by entering 'b'.
    If the password is correct, then login.

    """
    menutree = caller.ndb._menutree
    string_input = string_input.strip()

    # Check the password and login is correct; also check for bans

    account = menutree.account
    password_attempts = menutree.password_attempts \
        if hasattr(menutree, "password_attempts") else 0
    bans = ServerConfig.objects.conf("server_bans")
    banned = bans and (any(tup[0] == account.name.lower() for tup in bans) or
                       any(tup[2].match(caller.address) for tup in bans if tup[2]))

    if not account.check_password(string_input):
        # Didn't enter a correct password
        password_attempts += 1
        if password_attempts > 2:
            # Too many tries
            caller.sessionhandler.disconnect(
                caller, "|rToo many failed attempts. Disconnecting.|n")
            text = ""
            options = {}
        else:
            menutree.password_attempts = password_attempts
            text = dedent("""
                |rIncorrect password.|n
                Try again or leave empty to go back.
            """.strip("\n"))
            # Loops on the same node
            options = (
                {"key": "",
                 "exec": lambda caller: caller.msg("", options={"echo": True}),
                 "goto": "start"},
                {"key": "_default",
                 "goto": "ask_password"})
    elif banned:
        # This is a banned IP or name!
        string = dedent("""
            |rYou have been banned and cannot continue from here.
            If you feel this ban is in error, please email an admin.|n
            Disconnecting.
        """.strip("\n"))
        caller.sessionhandler.disconnect(caller, string)
        text = ""
        options = {}
    else:
        # We are OK, log us in.
        text = ""
        options = {}
        caller.msg("", options={"echo": True})
        caller.sessionhandler.login(caller, account)

    return text, options


def create_account(caller):
    """Create a new account.

    This node simply prompts the user to entere a username.
    The input is redirected to 'create_username'.

    """
    text = "Enter your new account name."
    options = (
        {"key": "_default",
         "goto": "create_username"},)
    return text, options


def create_username(caller, string_input):
    """Prompt to enter a valid username (one that doesnt exist).

    'string_input' contains the new username.  If it exists, prompt
    the username to retry or go back to the login screen.

    """
    menutree = caller.ndb._menutree
    string_input = string_input.strip()
    account = managers.accounts.get_account_from_name(string_input)

    # If an account with that name exists, a new one will not be created
    if account:
        text = dedent("""
            |rThe account {} already exists.|n
            Enter another username or leave blank to go back.
        """.strip("\n")).format(string_input)
        # Loops on the same node
        options = (
            {"key": "",
             "goto": "start"},
            {"key": "_default",
             "goto": "create_username"})
    elif not RE_VALID_USERNAME.search(string_input):
        text = dedent("""
            |rThis username isn't valid.|n
            Only letters are accepted, without special characters.
            The username must be at least 3 characters long.
            Enter another username or leave blank to go back.
        """.strip("\n"))
        options = (
            {"key": "",
             "goto": "start"},
            {"key": "_default",
             "goto": "create_username"})
    else:
        # a valid username - continue getting the password
        menutree.accountname = string_input
        # Disables echo for entering password
        caller.msg("", options={"echo": False})
        # Redirects to the creation of a password
        text = "Enter this account's new password."
        options = (
            {"key": "_default",
             "goto": "create_password"},)

    return text, options


def create_password(caller, string_input):
    """Ask the user to create a password.

    This node is at the end of the menu for account creation.  If
    a proper MULTI_SESSION is configured, a character is also
    created with the same name (we try to login into it).

    """
    menutree = caller.ndb._menutree
    text = ""
    options = (
        {"key": "",
         "exec": lambda caller: caller.msg("", options={"echo": True}),
         "goto": "start"},
        {"key": "_default",
         "goto": "create_password"})

    password = string_input.strip()
    accountname = menutree.accountname

    if len(password) < LEN_PASSWD:
        # The password is too short
        text = dedent("""
            |rYour password must be at least {} characters long.|n
            Enter another password or leave it empty to go back.
        """.strip("\n")).format(LEN_PASSWD)
    else:
        # Everything's OK.  Create the new player account and
        # possibly the character, depending on the multisession mode
        from evennia.commands.default import unloggedin
        # We make use of the helper functions from the default set here.
        try:
            permissions = settings.PERMISSION_ACCOUNT_DEFAULT
            typeclass = settings.BASE_CHARACTER_TYPECLASS
            new_account = unloggedin._create_account(caller, accountname,
                                                     password, permissions)
            if new_account:
                if settings.MULTISESSION_MODE < 2:
                    default_home = ObjectDB.objects.get_id(
                        settings.DEFAULT_HOME)
                    unloggedin._create_character(caller, new_account,
                                                 typeclass, default_home, permissions)
        except Exception:
            # We are in the middle between logged in and -not, so we have
            # to handle tracebacks ourselves at this point. If we don't, we
            # won't see any errors at all.
            caller.msg(dedent("""
                |rAn error occurred.|n  Please e-mail an admin if
                the problem persists. Try another password or leave
                it empty to go back to the login screen.
            """.strip("\n")))
            logger.log_trace()
        else:
            text = ""
            caller.msg("|gWelcome, your new account has been created!|n")
            caller.msg("", options={"echo": True})
            caller.sessionhandler.login(caller, new_account)

    return text, options


def quit(caller):
    caller.sessionhandler.disconnect(caller, "Goodbye! Logging off.")
    return "", {}

# Other functions


def _formatter(nodetext, optionstext, caller=None):
    """Do not display the options, only the text.

    This function is used by EvMenu to format the text of nodes.
    Options are not displayed for this menu, where it doesn't often
    make much sense to do so.  Thus, only the node text is displayed.

    """
    return nodetext


# 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):
        "Execute the menu"
        EvMenu(self.caller, "evennia.contrib.menu_login",
               startnode="start", auto_look=False, auto_quit=False,
               cmd_on_exit=None, node_formatter=_formatter)
