from typing import Any, Text, Dict, List

from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.events import SlotSet, ActionExecuted, UserUttered, FollowupAction, Form, AllSlotsReset
from rasa_sdk.forms import FormAction
from rasa_sdk import utils
import logging
logger = logging.getLogger(__name__)

import numpy as np
import pickle
import pandas as pd
import re
import csv
import os

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

import spacy
# import fr_core_news_md
from spacy.attrs import IS_ALPHA, IS_STOP, IS_PUNCT
nlp = spacy.load("fr_core_news_md")

ENIGMA_PKL = os.path.join("enigma.pkl")

BOT_USER_INFO = os.path.join("action_data", "bot_user_information.csv")

RIDDLE_RESULTS = os.path.join("action_data", "riddle_results.csv")

SERVER = 'ssl0.ovh.net'
PORT = 465
SENDER = os.environ.get('LOGIN_ENIGMASTRAS_MAILBOX')
PSW = os.environ.get('PASSWORD_ENIGMASTRAS_MAILBOX')
LOGO_SIGNATURE = os.path.join("action_data", "logo_signature.jpg")

def get_riddle(enigma_df: pd.core.frame.DataFrame, category: str):
    """
    for a given category, will return a random riddle, its name and its solution
    from the enigma_df dataframe
    """

    enigma_cat = enigma_df[enigma_df['Category'] == category]
    e = np.random.randint(0, enigma_cat.shape[0])

    riddle_name = enigma_cat['Title'].iloc[e]
    riddle = enigma_cat['Riddle'].iloc[e]
    solution = enigma_cat['Solution'].iloc[e]
    token_solution_len = enigma_cat['Solution_length'].iloc[e]

    return (riddle_name, riddle, solution, token_solution_len)

def preprocess_spacy(sent: str) -> np.ndarray:
    """
    string preprocessing function which returns a numpy array of tokens
    """
    doc = nlp(sent)
    tokens = np.char.lower(np.array([token.text for token in doc]))
    return tokens[~doc.to_array([IS_STOP]).astype(bool) * \
~doc.to_array([IS_PUNCT]).astype(bool)]

def cosine_test(sentence_1: str, sentence_2: str) -> float:
    """
    return a value which is a similarity indicator to check riddle solutions
    based on cosine similarity
    """

    sentence_1 = " ".join(preprocess_spacy(sentence_1))
    sentence_2 = " ".join(preprocess_spacy(sentence_2))

    vectorizer = TfidfVectorizer()
    tf_idf = vectorizer.fit_transform([sentence_1, sentence_2]).toarray()
    pd.DataFrame(data = tf_idf, columns=vectorizer.get_feature_names())

    return cosine_similarity(tf_idf)[1, 0]

def spacy_vec_sim_test(sentence_1: str, sentence_2: str) -> float:
    """
    return a value which is a similarity indicator to check riddle solutions
    based on spacy vector similarity
    """
    sentence_1 = " ".join(preprocess_spacy(sentence_1))
    sentence_2 = " ".join(preprocess_spacy(sentence_2))

    sentence_1 = nlp(sentence_1)
    sentence_2 = nlp(sentence_2)

    sim = list()
    for token_1 in sentence_1:
        for token_2 in sentence_2:
            sim.append(token_1.similarity(token_2))
    sim = np.array(sim)

    if len(sim) == 0:
        return 0
    elif len(sim) == 1:
        return sim[0]
    else:
        return (np.sort(sim)[-1] + np.sort(sim)[-2]) / 2

def bot_riddle_answer_message(mark: int):
    """
    Get a int 0 <= mark <= 4
    return:
    0: the answer is wrong
    1: the answer is probably wrong
    2: not clear
    3: the answer is probably right
    4: the answer is right
    """

    messages = list()

    if mark == 0:
        messages.append("Dommage, c'est une mauvaise réponse !")
        messages.append("Et non, mauvaise réponse")

    elif mark == 1:
        messages.append("J'ai bien peur que cela soit une mauvaise réponse.")
        messages.append("Il me semble que cela soit une mauvaise réponse.")
        messages.append("Je crois que c'est une mauvaise réponse, désolé si je fais erreur, je suis encore en phase d'apprentissage.")

    elif mark == 2:
        messages.append("Je ne suis pas tout à fait sûr de ta réponse.")
        messages.append("La réponse est difficile à évaluer.")
        messages.append("Je n'arrive pas à évaluer la répopnse, excusez-moi, il me faut encore un peu de temps pour que je peaufine mon apprentissage.")

    elif mark == 3:
        messages.append("Sans en être entièrement sûr, je crois que ta réponse est correcte.")
        messages.append("Pas sûr et certains, mais je crois que ta réponse est correcte.")
        messages.append("Il me semble que la réponse est exacte. Je suis encore en phase d'apprentissage, merci de votre indulgnce en cas d'erreur de ma part.")

    elif mark == 4:
        messages.append("C'est une excellente réponse !")
        messages.append("Bravo, c'est une bonne réponse !")

    message = np.random.choice(np.array(messages))
    return message

def compare_answer(solution: str, user_solution: str) -> tuple:
    """
    based on the cosine_test and spacy_vec_sim_test inicators, will returns
    wether the user answer to the riddle seems to be right or wrong.
    Return:
    - cosine_indicator: indicator based on cosine similarity
    - spacy_vec_sim_indicator: indicator based on spacy vector similarity
    - mark: mark attributed to the user solution
    - bot_riddle_answer_message(mark): answer message to display
    """
    # Cosine similarity indicator
    try:
        cosine_indicator = cosine_test(solution, user_solution)
    except:
        cosine_indicator = 0

    # Spacy vector similarity indicator
    spacy_vec_sim_indicator = spacy_vec_sim_test(solution, user_solution)

    mark = int

    if len(solution) == 1: # Only one token in the solution
        if cosine_indicator >= 1 and spacy_vec_sim_indicator >= 1:
            mark = 4

        if cosine_indicator > .6 or (cosine_indicator > 0 and spacy_vec_sim_indicator > .7):
            mark = 3

        if cosine_indicator > .3 or (cosine_indicator > 0 and spacy_vec_sim_indicator > .5):
            mark = 2

        elif cosine_indicator == 0 and spacy_vec_sim_indicator == 0:
            mark = 0

        else:
            mark = 1

    elif len(solution) == 2: # Excatly two tokens in the solution
        if cosine_indicator >= 1 and spacy_vec_sim_indicator >= 1:
            mark = 4

        elif cosine_indicator > .1 or spacy_vec_sim_indicator > .45:
            mark = 3

        elif cosine_indicator > .05 or spacy_vec_sim_indicator > .3:
            mark = 2

        elif cosine_indicator == 0 and spacy_vec_sim_indicator == 0:
            mark = 0

        else:
            mark = 1

    else: # More than two tokens in the solution
        if cosine_indicator >= 1 and spacy_vec_sim_indicator >= 1:
            mark = 4

        elif cosine_indicator > .5 or spacy_vec_sim_indicator > .5:
            mark = 3

        elif cosine_indicator > .2 or spacy_vec_sim_indicator > .2:
            mark = 2

        elif cosine_indicator == 0 and spacy_vec_sim_indicator == 0:
            mark = 0

        else:
            mark = 1

    return cosine_indicator, spacy_vec_sim_indicator, mark, bot_riddle_answer_message(mark)

def save_information(user_name: str, user_email: str):
    with open(BOT_USER_INFO, "a") as f:
        writer = csv.writer(f)
        writer.writerow([user_name, user_email])
    return None

def save_riddle_results(riddle: str, solution: str, user_solution: str, token_solution_len: int, cosine_indicator: int, spacy_vec_sim_indicator: int, mark: int):

    row = [riddle, solution, user_solution, token_solution_len, cosine_indicator, spacy_vec_sim_indicator, mark]

    with open(RIDDLE_RESULTS, "a") as f:
        writer = csv.writer(f)
        writer.writerow(row)

    return None

class ActionWhichGame(Action):

    def name(self) -> Text:
        return "action_which_game"

    def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:

        which_game = "Il existe deux parcours, par quel jeu êtes-vous intéressé ?"
        game_1 = "La Société Mystérieuse de Strasbourg"
        game_2 = "1913, Meurte à la Krutenau"

        buttons = [{"title": game_1, "payload": game_1},
            {"title": game_2, "payload": game_2}]

        dispatcher.utter_button_message(which_game, buttons)

        return []

class ActionRequestedGame(Action):

    def name(self) -> Text:
        return "action_requested_game"

    def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:
        game = tracker.get_slot("game")

        if game.lower() == "société mystérieuse de Strasbourg".lower():
            dispatcher.utter_message(template="utter_societe_mysterieuse")
            dispatcher.utter_message(template="utter_next_game")
        else :
            dispatcher.utter_message(template="utter_meurtre_krutenau")
            dispatcher.utter_message(template="utter_next_game")

        return []

class ActionNextGame(Action):

    def name(self) -> Text:
        return "action_next_game"

    # @staticmethod
    # def start_story_events(deny):
    #     # type: (Text) -> List[Dict]
    #     return [ActionExecuted("action_listen")] + [UserUttered("/" + deny, {
    #         "intent": {"name": deny, "confidence": 1.0},
    #         "entities": {}
    #     })]
    # Trigger intent from a custom action:
    # https://forum.rasa.com/t/trigger-a-story-or-intent-from-a-custom-action/13784

    def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:

        deny_or_affirm = tracker.latest_message['intent'].get('name')

        if deny_or_affirm == 'affirm':

            game = tracker.get_slot("game")

            if game.lower() == "société mystérieuse de Strasbourg".lower():
                dispatcher.utter_message(template="utter_meurtre_krutenau")
            else :
                dispatcher.utter_message(template="utter_societe_mysterieuse")

        return []

class ActionDefaultFallback(Action):

    def name(self) -> Text:
        return "action_default_fallback"

    def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:

        active_form = tracker.active_form.get("name")

        if active_form == "form_riddle":
            logger.debug("The form '{}' is active".format(active_form))

            # Riddle form is active, reask for the solution

            if tracker.events[-5].get('name') == 'utter_ask_leave_riddle_form':

                # case if user asked a faq or oos question in the time being

                logger.debug("The form '{}' is active".format(active_form))
                riddle = tracker.get_slot('riddle')
                message = f"je vous rappelle l'énigme:\n{riddle}"
                dispatcher.utter_message(message)

            else:
                if not tracker.latest_message.get('text').startswith("rep"):
                    message = "Je n'ai pas pu interprêter votre réponse, pourriez-vous écrire \"Rep:\" puis écrire votre réponse ?"
                    dispatcher.utter_message(message)
                else:
                    message = "Je n'ai pas pu interprêter votre réponse, pourriez-vous écrire \"Rep:[espace]\" puis écrire votre réponse ?"
                    dispatcher.utter_message(message)

            return []

        elif active_form == "form_subscribe":
            requested_slot = tracker.get_slot("requested_slot")

            # Subscribe form is active, reask for the requested slots

            if tracker.events[-5].get('name') == 'utter_ask_leave_subscribe_form':

                # case if user asked a faq or oos question in the time being

                logger.debug("The form '{}' is active".format(active_form))
                dispatcher.utter_message(template=f'utter_ask_{requested_slot}')

            else:
                if requested_slot == "user_name":
                    logger.debug("The form '{}' is active".format(active_form))

                    message = "Je ne connais pas ce prénom, pourriez-vous écrire \"prénom:\" puis taper votre prénom, afin que je puisse le reconnaître?"
                    dispatcher.utter_message(message)

                elif requested_slot == "user_email":
                    logger.debug("The form '{}' is active".format(active_form))

                    message = "Je ne reconnais pas le format de l'adresse mail, pourriez-vous écrire \"e-mail:\" puis taper votre adresse e-mail, afni que je puisse la reconnaître?"

                    dispatcher.utter_message(message)

            return[]

        else:
            logger.debug("There is no active form")

            messages = []

            messages.append("Je ne suis pas sûr de comprendre, pourriez-vous reformuler ?")
            messages.append("Je suis désolé, je n'ai pas compris. Est-il possible de reformuler la quesion ?")
            messages.append("Excusez-moi mais je n'ai pas compris ce que vous demandez, est-ce que vous pourriez reformuler ?")
            messages.append("\nAttends voir... \nNon y'a quelque chose que je n'ai pas compris. Après tout je suis encore en apprentissage. Mais si vous reformulez, ça pourrait m'aider.\n")
            messages.append("J'ai bien peur de ne pas avoir compris... Est-ce bien en rapport avec ENIGMA Strasbourg ?")
            messages.append("\nMhm, je ne suis pas sûr de bien saisir ce que vous cherchez à me demander. \nPeut-être qu'en reformulant j'arriverais à comprendre.\n")

            message = np.random.choice(np.array(messages))

            dispatcher.utter_message(message)

            return []

class FormRiddle(FormAction):
    """Custom form action to fill the user riddle answer."""

    def name(self) -> Text:
        """Unique identifier of the form"""
        return "form_riddle"

    @staticmethod
    def required_slots(tracker: Tracker) -> List[Text]:
        """A list of required slots that the form has to fill"""
        return ["riddle_category", "user_riddle_solution"]

    def request_next_slot(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:
        """Request the next slot and utter template if needed,
            else return None"""

        with open("enigma.pkl", 'rb') as f:
            enigma_df = pickle.load(f)

        categories = enigma_df['Category'].unique()
        buttons = list()
        for cat in categories:
            buttons.append({'title':cat, 'payload': cat})

        for slot in self.required_slots(tracker):
            if self._should_request_slot(tracker, slot):
                logger.debug("Request next slot '{}'".format(slot))

                if slot == "riddle_category":
                    messages = list()
                    messages.append("Quelle genre d'énigme aimez-vous ?\n")
                    messages.append("Quelle genre d'énigme ?\n")
                    messages.append("Quelle type d'énigme ?\n")
                    message = np.random.choice(np.array(messages))

                    dispatcher.utter_button_message(message, buttons)
                    return [SlotSet("requested_slot", slot)]

                if slot == "user_riddle_solution" and tracker.get_slot('riddle') is None:
                    category = tracker.get_slot('riddle_category')

                    riddle_name, riddle, solution, token_solution_len = get_riddle(enigma_df, category[0])

                    token_solution_len = str(token_solution_len)

                    how_to_answer = "(Commencez par \"rep:\" puis écrivez votre réponse.)"
                    message = f"**{riddle_name}** \n\
                            \n{riddle}\n\
                            \n{how_to_answer}\n"

                    dispatcher.utter_message(message)

                    return [SlotSet("requested_slot", slot), SlotSet("riddle_solution", solution), SlotSet("riddle", riddle), SlotSet("token_solution_len", token_solution_len)]

                if slot == "user_riddle_solution" and tracker.get_slot('riddle') is not None:
                    riddle = tracker.get_slot('riddle')

                    how_to_answer = "(Commencez par \"rep:\" puis écrivez votre réponse.)"

                    dispatcher.utter_message(f"Je vous rappelle l'énigme:\n\
                        \n{riddle}\n\
                        \n{how_to_answer}\n")

                    return [SlotSet("requested_slot", slot)]

        return None

    # def slot_mappings(self) -> Dict[Text, Any]:
    #     return {"riddle_category": self.from_entity(entity="riddle_category",
    #             intent=["inform_kind_of_riddle"])}

    def submit(self,
               dispatcher: CollectingDispatcher,
               tracker: Tracker,
               domain: Dict[Text, Any]
               ) -> List[Dict]:
        """Once required slots are filled, print the message"""

        user_answer = tracker.latest_message.get('text')
        user_riddle_solution = re.sub(r'Rep:.', '', user_answer)
        riddle_solution = tracker.get_slot('riddle_solution')
        riddle = tracker.get_slot('riddle')
        token_solution_len = tracker.get_slot('token_solution_len')

        cosine_indicator, spacy_vec_sim_indicator, mark, message = compare_answer(riddle_solution, user_riddle_solution)

        save_riddle_results(riddle, riddle_solution, user_riddle_solution, token_solution_len, cosine_indicator, spacy_vec_sim_indicator, mark)

        dispatcher.utter_message(message)
        dispatcher.utter_message(f"La bonne réponse est : \n{riddle_solution}")

        return [SlotSet("riddle_category", None), SlotSet("riddle_solution", None), SlotSet("user_riddle_solution", None), SlotSet("riddle", None), SlotSet("token_solution_len", None)]

class ActionResetRiddleSlots(Action):

    def name(self):
        return "action_reset_riddle_slots"

    def run(self, dispatcher, tracker, domain):
        return [SlotSet("riddle_category", None)]

class FormSubscribe(FormAction):
    """Custom form action to fill the user riddle answer."""

    def name(self) -> Text:
        """Unique identifier of the form"""
        return "form_subscribe"

    @staticmethod
    def required_slots(tracker: Tracker) -> List[Text]:
        """A list of required slots that the form has to fill"""
        return ["user_name", "user_email"]

    def slot_mappings(self) -> Dict[Text, Any]:
        return {"user_name": self.from_entity(
                    entity="user_name",
                    intent=["inform_name"]),
                "user_email": self.from_entity(
                    entity="user_email",
                    intent=["inform_email"])
                }

    def submit(self,
               dispatcher: CollectingDispatcher,
               tracker: Tracker,
               domain: Dict[Text, Any]
               ) -> List[Dict]:
        """Once required slots are filled, print the message"""

        return []

class ActionAskUserToCheckInfomration(Action):

    def name(self) -> Text:
        return "action_ask_user_to_check_infomration"

    def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:
        name = tracker.get_slot('user_name')
        if type(name) == list:
            name = name[0]

        email = tracker.get_slot('user_email')
        if type(email) == list:
            email = email[0]

        message = f"\n* **Prénom :** {name.capitalize()}\n\
        \n* **e-mail :** {email}\n\
        \nPouvez-vous confirmez que ces informations sont correctes ?"

        dispatcher.utter_message(message)

        return []

class ActionResetSubscribeSlots(Action):

    def name(self):
        return "action_reset_subscribe_slots"

    def run(self, dispatcher, tracker, domain):
        return [SlotSet("user_name", None), SlotSet("user_email", None)]

class ActionSaveInformation(Action):

    def name(self) -> Text:
        return "action_save_information"

    def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:

        user_name = tracker.get_slot('user_name')
        if type(user_name) == list:
            user_name = user_name[0]

        user_email = tracker.get_slot('user_email')
        if type(user_email) == list:
            user_email = user_email[0]

        # Create the root message and fill in the from, to, and subject headers
        msgRoot = MIMEMultipart('related')
        msgRoot['Subject'] = 'ENIGMA Strasbourg - merci d\'avoir testé Enigmabot'
        msgRoot['From'] = SENDER
        msgRoot['To'] = user_email

        # Encapsulate the plain and HTML versions of the message body in an
        # 'alternative' part, so message agents can decide which they want to display.
        msgAlternative = MIMEMultipart('alternative')
        msgRoot.attach(msgAlternative)

        # We reference the image in the IMG SRC attribute by the ID we give it below
        body = f"""\
        <html>
            <body>
                <h2>Bienvenue {user_name.capitalize()} !</h2>
                <p>Votre adresse e-mail a bien été enregistrée et vous recevrez prochainement les actualités d\'ENIGMA Strasbourg.</p>
                <p>Merci d\'avoir tester Enigmabot 🤖, cela permet d\'améliorer ses capacités.</p>
                <p>Mystérieusement</p>
                <a href="https://enigmastrasbourg.com/"> <img src="cid:image1" alt="logo_signature" style="width:320px;height:120px;"></a>
            </body>
        </html>
        """
        msgText = MIMEText(body, 'html')
        msgAlternative.attach(msgText)

        # This example assumes the image is in the current directory
        fp = open(LOGO_SIGNATURE, 'rb')
        msgImage = MIMEImage(fp.read())
        fp.close()

        # Define the image's ID as referenced above
        msgImage.add_header('Content-ID', '<image1>')
        msgRoot.attach(msgImage)

        # Send the email (this example assumes SMTP authentication is required)
        with smtplib.SMTP_SSL(SERVER, PORT) as smtp:
            smtp.login(SENDER, PSW)
            smtp.sendmail(SENDER, user_email, msgRoot.as_string())

        save_information(user_name, user_email)

        message = "Votre adresse e-mail est bien enregistrée, vous allez recevoir un mail de confirmation."

        dispatcher.utter_message(message)

        return []

class ActionDeactivateForm(Action):

    def name(self):
        return "action_deactivate_form"

    def run(self, dispatcher, tracker, domain):
        return [Form(None), AllSlotsReset()]

class ActionCheckIfAlreadySubscribed(Action):

    def name(self):
        return "action_check_if_already_subscribed"

    def run(self, dispatcher, tracker, domain):
        if tracker.get_slot("user_email") is None:
            return []

        else:
            dispatcher.utter_message(template="utter_ok")
            dispatcher.utter_message(template="utter_thanks")
            dispatcher.utter_message(template="utter_goodbye")
            return [ActionExecuted("action_listen")]
