"""
Link Evennia to external resources (wizard plugin for evennia_launcher)

"""
import pprint
import sys
from os import path

from django.conf import settings

from evennia.utils.utils import list_to_string, mod_import


class ConnectionWizard(object):
    def __init__(self):
        self.data = {}
        self.prev_node = None

    def display(self, text):
        "Show text"
        print(text)

    def ask_continue(self):
        "'Press return to continue'-prompt"
        input(" (Press return to continue)")

    def ask_node(self, options, prompt="Enter choice: ", default=None):
        """
        Retrieve options and jump to different menu nodes

        Args:
            options (dict): Node options on the form {key: (desc, callback), }
            prompt (str, optional): Question to ask
            default (str, optional): Default value to use if user hits return.

        """

        opt_txt = "\n".join(f" {key}: {desc}" for key, (desc, _, _) in options.items())
        self.display(opt_txt + "\n")

        while True:
            resp = input(prompt).strip()

            if not resp:
                if default:
                    resp = str(default)

            if resp.lower() in options:
                # self.display(f" Selected '{resp}'.")
                desc, callback, kwargs = options[resp.lower()]
                callback(self, **kwargs)
            elif resp.lower() in ("quit", "q"):
                sys.exit()
            elif resp:
                # input, but nothing was recognized
                self.display(" Choose one of: {}".format(list_to_string(list(options))))

    def ask_yesno(self, prompt, default="yes"):
        """
        Ask a yes/no question inline.

        Keyword Args:
            prompt (str): The prompt to ask.
            default (str): "yes" or "no", used if pressing return.
        Returns:
            reply (str): Either 'yes' or 'no'.

        """
        prompt = prompt + (" [Y]/N? " if default == "yes" else " Y/[N]? ")

        while True:
            resp = input(prompt).lstrip().lower()
            if not resp:
                resp = default.lower()
            if resp in ("yes", "y"):
                self.display(" Answered Yes.")
                return "yes"
            elif resp in ("no", "n"):
                self.display(" Answered No.")
                return "no"
            elif resp.lower() in ("quit", "q"):
                sys.exit()

    def ask_choice(self, prompt=" > ", options=None, default=None):
        """
        Ask multiple-choice question, get response inline.

        Keyword Args:
            prompt (str): Input prompt.
            options (list): List of options. Will be indexable by sequence number 1...
            default (int): The list index+1 of the default choice, if any
        Returns:
            reply (str): The answered reply.

        """
        opt_txt = "\n".join(f" {ind + 1}: {desc}" for ind, desc in enumerate(options))
        self.display(opt_txt + "\n")

        while True:
            resp = input(prompt).strip()

            if not resp:
                if default:
                    return options[int(default)]
            if resp.lower() in ("quit", "q"):
                sys.exit()
            if resp.isdigit():
                resp = int(resp) - 1
                if 0 <= resp < len(options):
                    selection = options[resp]
                    self.display(f" Selected '{selection}'.")
                    return selection
            self.display(" Select one of the given options.")

    def ask_input(self, prompt=" > ", default=None, validator=None):
        """
        Get arbitrary input inline.

        Keyword Args:
            prompt (str): The display prompt.
            default (str): If empty input, use this.
            validator (callable): If given, the input will be passed
                into this callable. It should return True unless validation
                fails (and is expected to echo why if so).

        Returns:
            inp (str): The input given, or default.

        """
        while True:
            resp = input(prompt).strip()

            if not resp and default:
                resp = str(default)

            if resp.lower() in ("q", "quit"):
                sys.exit()

            if resp.lower() == "none":
                resp = ""

                if validator and not validator(resp):
                    continue

                ok = input("\n Leave blank? [Y]/N: ")
                if ok.lower() in ("n", "no"):
                    continue
                elif ok.lower() in ("q", "quit"):
                    sys.exit()
                return resp

            if validator and not validator(resp):
                continue

            self.display(resp)
            ok = input("\n Is this correct? [Y]/N: ")
            if ok.lower() in ("n", "no"):
                continue
            elif ok.lower() in ("q", "quit"):
                sys.exit()
            return resp


def node_start(wizard):
    text = """
    This wizard helps to attach your Evennia server to external networks. It
    will save to a file `server/conf/connection_settings.py` that will be
    imported from the bottom of your game settings file. Once generated you can
    also modify that file directly.

    Make sure you have at least started the game once before continuing!

    Use `quit` at any time to abort and throw away unsaved changes.
    """
    options = {
        "1": (
            "Add your game to the Evennia game index (also for closed-dev games)",
            node_game_index_start,
            {},
        ),
        "2": ("MSSP setup (for mud-list crawlers)", node_mssp_start, {}),
        # "3": ("Add Grapevine listing",
        #       node_grapevine_start, {}),
        # "4": ("Add IRC link",
        #      "node_irc_start", {}),
        # "5" ("Add RSS feed",
        #       "node_rss_start", {}),
        "s": ("View and (optionally) Save created settings", node_view_and_apply_settings, {}),
        "q": ("Quit", lambda *args: sys.exit(), {}),
    }

    wizard.display(text)
    wizard.ask_node(options)


# Evennia game index


def node_game_index_start(wizard, **kwargs):
    text = """
    The Evennia game index (http://games.evennia.com) lists both active Evennia
    games as well as games in various stages of development.

    You can put up your game in the index also if you are not (yet) open for
    players.  If so, put 'None' for the connection details - you are just telling
    us that you are out there, making us excited about your upcoming game!

    Please check the listing online first to see that your exact game name is
    not colliding with an existing game-name in the list (be nice!).
    """

    wizard.display(text)
    if wizard.ask_yesno("Continue adding/editing an Index entry?") == "yes":
        node_game_index_fields(wizard)
    else:
        node_start(wizard)


def node_game_index_fields(wizard, status=None):
    # reset the listing if needed
    if not hasattr(wizard, "game_index_listing"):
        wizard.game_index_listing = settings.GAME_INDEX_LISTING

    # game status

    status_default = wizard.game_index_listing["game_status"]
    text = f"""
    What is the status of your game?
        - pre-alpha: a game in its very early stages, mostly unfinished or unstarted
        - alpha: a working concept, probably lots of bugs and incomplete features
        - beta: a working game, but expect bugs and changing features
        - launched: a full, working game (that may still be expanded upon and improved later)

    Current value (return to keep):
    {status_default}
    """

    options = ["pre-alpha", "alpha", "beta", "launched"]

    wizard.display(text)
    wizard.game_index_listing["game_status"] = wizard.ask_choice("Select one: ", options)

    # game name

    name_default = settings.SERVERNAME
    text = f"""
    Your game's name should usually be the same as `settings.SERVERNAME`, but
    you can set it to something else here if you want.

    Current value:
    {name_default}
    """

    def name_validator(inp):
        tmax = 80
        tlen = len(inp)
        if tlen > tmax:
            print(f"The name must be shorter than {tmax} characters (was {tlen}).")
            wizard.ask_continue()
            return False
        return True

    wizard.display(text)
    wizard.game_index_listing["game_name"] = wizard.ask_input(
        default=name_default, validator=name_validator
    )

    # short desc

    sdesc_default = wizard.game_index_listing.get("short_description", None)

    text = f"""
    Enter a short description of your game. Make it snappy and interesting!
    This should be at most one or two sentences (255 characters) to display by
    '{settings.SERVERNAME}' in the main game list. Line breaks will be ignored.

    Current value:
    {sdesc_default}
    """

    def sdesc_validator(inp):
        tmax = 255
        tlen = len(inp)
        if tlen > tmax:
            print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")
            wizard.ask_continue()
            return False
        return True

    wizard.display(text)
    wizard.game_index_listing["short_description"] = wizard.ask_input(
        default=sdesc_default, validator=sdesc_validator
    )

    # long desc

    long_default = wizard.game_index_listing.get("long_description", None)

    text = f"""
    Enter a longer, full-length description. This will be shown when clicking
    on your game's listing. You can use \\n to create line breaks and may use
    Markdown formatting like *bold*, _italic_, [linkname](http://link) etc.

    Current value:
    {long_default}
    """

    wizard.display(text)
    wizard.game_index_listing["long_description"] = wizard.ask_input(default=long_default)

    # listing contact

    listing_default = wizard.game_index_listing.get("listing_contact", None)
    text = f"""
    Enter a listing email-contact. This will not be visible in the listing, but
    allows us to get in touch with you should there be some listing issue (like
    a name collision) or some bug with the listing (us actually using this is
    likely to be somewhere between super-rarely and never).

    Current value:
    {listing_default}
    """

    def contact_validator(inp):
        if not inp or "@" not in inp:
            print("This should be an email and cannot be blank.")
            wizard.ask_continue()
            return False
        return True

    wizard.display(text)
    wizard.game_index_listing["listing_contact"] = wizard.ask_input(
        default=listing_default, validator=contact_validator
    )

    # telnet hostname

    hostname_default = wizard.game_index_listing.get("telnet_hostname", None)
    text = f"""
    Enter the hostname to which third-party telnet mud clients can connect to
    your game. This would be the name of the server your game is hosted on,
    like `coolgame.games.com`, or `mygreatgame.se`.

    Write 'None' if you are not offering public telnet connections at this time.

    Current value:
    {hostname_default}
    """

    wizard.display(text)
    wizard.game_index_listing["telnet_hostname"] = wizard.ask_input(default=hostname_default)

    # telnet port

    port_default = wizard.game_index_listing.get("telnet_port", None)
    text = f"""
    Enter the main telnet port. The Evennia default is 4000. You can change
    this with the TELNET_PORTS server setting.

    Write 'None' if you are not offering public telnet connections at this time.

    Current value:
    {port_default}
    """

    wizard.display(text)
    wizard.game_index_listing["telnet_port"] = wizard.ask_input(default=port_default)

    # website

    website_default = wizard.game_index_listing.get("game_website", None)
    text = f"""
    Evennia is its own web server and runs your game's website. Enter the
    URL of the website here, like http://yourwebsite.com, here.

    Write 'None' if you are not offering a publicly visible website at this time.

    Current value:
    {website_default}
    """

    wizard.display(text)
    wizard.game_index_listing["game_website"] = wizard.ask_input(default=website_default)

    # webclient

    webclient_default = wizard.game_index_listing.get("web_client_url", None)
    text = f"""
    Evennia offers its own native webclient. Normally it will be found from the
    game homepage at something like http://yourwebsite.com/webclient. Enter
    your specific URL here (when clicking this link you should launch into the
    web client)

    Write 'None' if you don't want to list a publicly accessible webclient.

    Current value:
    {webclient_default}
    """

    wizard.display(text)
    wizard.game_index_listing["web_client_url"] = wizard.ask_input(default=webclient_default)

    if not (
        wizard.game_index_listing.get("web_client_url")
        or (wizard.game_index_listing.get("telnet_host"))
    ):
        wizard.display(
            "\nNote: You have not specified any connection options. This means "
            "your game \nwill be marked as being in 'closed development' in "
            "the index."
        )

    wizard.display("\nDon't forget to inspect and save your changes.")

    node_start(wizard)


# MSSP


def node_mssp_start(wizard):
    mssp_module = mod_import(settings.MSSP_META_MODULE or "server.conf.mssp")
    try:
        filename = mssp_module.__file__
    except AttributeError:
        filename = "server/conf/mssp.py"

    text = f"""
    MSSP (Mud Server Status Protocol) has a vast amount of options so it must
    be modified outside this wizard by directly editing its config file here:

        '{filename}'

    MSSP allows traditional online MUD-listing sites/crawlers to continuously
    monitor your game and list information about it. Some of this, like active
    player-count, Evennia will automatically add for you, whereas most fields
    you need to set manually.

    To use MSSP you should generally have a publicly open game that external
    players can connect to. You also need to register at a MUD listing site to
    tell them to crawl your game.
    """

    wizard.display(text)
    wizard.ask_continue()
    node_start(wizard)


# Admin


def _save_changes(wizard):
    """
    Perform the save
    """

    # add import statement to settings file
    import_stanza = "from .connection_settings import *"
    setting_module = mod_import("server.conf.settings")
    with open(setting_module.__file__, "r+") as f:
        txt = f.read()  # moves pointer to end of file
        if import_stanza not in txt:
            # add to the end of the file
            f.write(
                "\n\n"
                "try:\n"
                "    # Created by the `evennia connections` wizard\n"
                f"    {import_stanza}\n"
                "except ImportError:\n"
                "    pass"
            )

    connect_settings_file = path.join(settings.GAME_DIR, "server", "conf", "connection_settings.py")
    with open(connect_settings_file, "w") as f:
        f.write(
            "# This file is auto-generated by the `evennia connections` wizard.\n"
            "# Don't edit manually, your changes will be overwritten.\n\n"
        )

        f.write(wizard.save_output)
        wizard.display(f"saving to {connect_settings_file} ...")


def node_view_and_apply_settings(wizard):
    """
    Inspect and save the data gathered in the other nodes

    """
    pp = pprint.PrettyPrinter(indent=4)
    saves = False

    # game index
    game_index_save_text = ""
    game_index_listing = (
        wizard.game_index_listing if hasattr(wizard, "game_index_listing") else None
    )
    if not game_index_listing and settings.GAME_INDEX_ENABLED:
        game_index_listing = settings.GAME_INDEX_LISTING
    if game_index_listing:
        game_index_save_text = (
            "GAME_INDEX_ENABLED = True\n"
            "GAME_INDEX_LISTING = \\\n" + pp.pformat(game_index_listing)
        )
        saves = True
    else:
        game_index_save_text = "# No Game Index settings found."

    # potentially add other wizards in the future
    text = game_index_save_text

    wizard.display(f"Settings to save:\n\n{text}")

    if saves:
        if wizard.ask_yesno("\nDo you want to save these settings?") == "yes":
            wizard.save_output = text
            _save_changes(wizard)
            wizard.display("... saved!\nThe changes will apply after you reload your server.")
        else:
            wizard.display("... cancelled.")
    wizard.ask_continue()
    node_start(wizard)
