from django.db import models
import re
import json
import os
import csv
import pandas

def markdownToQuilldelta(markdown_text):
    delta = {"ops": []}

    # Fonction pour ajouter des opérations au Delta
    def add_op(insert, attributes=None):
        op = {"insert": insert}
        if attributes:
            op["attributes"] = attributes
        delta["ops"].append(op)

    # Expressions régulières pour détecter les éléments Markdown
    patterns = {
        'header1': re.compile(r'^# (.*)', re.MULTILINE),
        'header2': re.compile(r'^## (.*)', re.MULTILINE),
        'header3': re.compile(r'^### (.*)', re.MULTILINE),
        'bold': re.compile(r'\*\*(.*?)\*\*'),
        'italic': re.compile(r'\*(.*?)\*'),
        'underline': re.compile(r'__(.*?)__'),
        'link': re.compile(r'\[(.*?)\]\((.*?)\)'),
        'bullet_list': re.compile(r'^[\-\*] (.*)', re.MULTILINE),
        'ordered_list': re.compile(r'^\d+\. (.*)', re.MULTILINE),
        'latex_block': re.compile(r'\$\$(.*?)\$\$', re.DOTALL),
        'latex_inline': re.compile(r'\\\((.*?)\\\)', re.DOTALL),  # Nouveau pattern pour LaTeX inline avec \( \)
        'formula': re.compile(r'\$(.*?)\$', re.DOTALL),
        'latex_display': re.compile(r'\\\[(.*?)\\\]', re.DOTALL)  # Pattern pour le mode display LaTeX
    }

        # Liste pour stocker les segments de texte et leurs types
    segments = []

    # Parcourir le texte Markdown et identifier les motifs
    pos = 0
    buffer = ""
    if len(markdown_text) > 9000:
        delta["ops"].append({"insert": "Text too long, error P051f"})
        return delta
    while pos < len(markdown_text) and pos <= 9000:
        char = markdown_text[pos]
        if char in ['_', '-', '*', '#', '[', ']', '(', ')', '$', '.', '\\', '/']:
            # Vérifier s'il y a un motif à enregistrer
            if buffer:
                segments.append(('text', buffer, ()))
                buffer = ""
            
            # Vérifier les motifs à partir de la position actuelle
            match = None
            for key, pattern in patterns.items():
                match = pattern.match(markdown_text, pos)
                if match:
                    segments.append((key, match.group(1), match.groups()))
                    pos = match.end() - 1  # Ajuster la position
                    break
            if not match:
                buffer += char
        else:
            buffer += char
        pos += 1
    
    # Parcourir les segments et les transformer en Delta de Quill
    for key, text, groups in segments:
        if key == 'header1':
            sub_delta = markdownToQuilldelta(text)
            delta["ops"].extend(sub_delta["ops"])
            add_op('\n', {"header": 1})
        elif key == 'header2':
            sub_delta = markdownToQuilldelta(text)
            delta["ops"].extend(sub_delta["ops"])
            add_op('\n', {"header": 2})
        elif key == 'header3':
            sub_delta = markdownToQuilldelta(text)
            delta["ops"].extend(sub_delta["ops"])
            add_op('\n', {"header": 3})
        elif key == 'bold':
            sub_delta = markdownToQuilldelta(text)
            for op in sub_delta["ops"]:
                if "attributes" in op:
                    op["attributes"]["bold"] = True
                else:
                    op["attributes"] = {"bold": True}
            delta["ops"].extend(sub_delta["ops"])
        elif key == 'italic':
            add_op(text, {"italic": True})
        elif key == 'underline':
            add_op(text, {"underline": True})
        elif key == 'link':
            add_op(groups[0], {"link": groups[1]})
        elif key == 'bullet_list':
            sub_delta = markdownToQuilldelta(text)
            # Éviter d'ajouter un retour à la ligne si le dernier caractère est déjà un retour à la ligne
            if delta["ops"] and isinstance(delta["ops"][-1].get("insert"), str) and delta["ops"][-1]["insert"].endswith('\n'):
                # Ajouter l'attribut de liste au dernier retour à la ligne
                if "attributes" not in delta["ops"][-1]:
                    delta["ops"][-1]["attributes"] = {}
                delta["ops"][-1]["attributes"]["list"] = "bullet"
                # Ajouter le contenu sans le retour à la ligne
                delta["ops"].extend(sub_delta["ops"])
            else:
                # Comportement normal si pas de retour à la ligne
                delta["ops"].extend(sub_delta["ops"])
                add_op('\n', {"list": "bullet"})
        elif key == 'ordered_list':
            sub_delta = markdownToQuilldelta(text)
            # Même logique pour les listes ordonnées
            if delta["ops"] and isinstance(delta["ops"][-1].get("insert"), str) and delta["ops"][-1]["insert"].endswith('\n'):
                if "attributes" not in delta["ops"][-1]:
                    delta["ops"][-1]["attributes"] = {}
                delta["ops"][-1]["attributes"]["list"] = "ordered"
                delta["ops"].extend(sub_delta["ops"])
            else:
                delta["ops"].extend(sub_delta["ops"])
                add_op('\n', {"list": "ordered"})
        elif key == 'latex_block':
            # add_op('\n', {"align": "center"})
            add_op({"formula": text}) 
            add_op('\n', {"align": "center"}) 
        elif key == 'latex_inline':
            add_op({"formula": text}) 
        elif key == 'latex_display':
            add_op({"formula": text}) 
        elif key == 'formula':
            add_op({"formula": groups[0]}) 
        else:
            add_op(text) 

    if buffer:
        add_op(buffer) 

    return delta

def closeJSON(json_string):
    stack = []
    in_string = False
    escape_next = False

    for char in json_string:
        if escape_next:
            escape_next = False
            continue
            
        if char == '\\':
            escape_next = True
            continue
            
        if char == '\"':
            if not in_string:
                in_string = True
                stack.append('\"')
            else:
                if stack and stack[-1] == '\"':
                    stack.pop()
                    in_string = False
                else:
                    stack.append('\"')
        elif not in_string:
            if char in ['{', '[']:
                stack.append(char)
            elif char in ['}', ']']:
                if stack and ((char == '}' and stack[-1] == '{') or (char == ']' and stack[-1] == '[')):
                    stack.pop()
                else:
                    return json_string  # JSON mal formé, ne peut pas être corrigé simplement

    # Ajouter les caractères manquants pour fermer le JSON
    while stack:
        open_char = stack.pop()
        if open_char == '{':
            json_string += '}'
        elif open_char == '[':
            json_string += ']'
        elif open_char == '\"':
            json_string += '\"'

    return json_string


def reformateJSON(json_string):
    """
    Reformate une chaîne JSON en supprimant les caractères non valides en dehors des zones de données.
    
    Args:
        json_string (str): La chaîne JSON à reformater
        
    Returns:
        str: La chaîne JSON reformatée
    """
    result = ""
    in_string = False
    in_value = False
    
    for i, char in enumerate(json_string): # La fonction n'a pas été testée
        # Gestion des chaînes de caractères
        if char in ['"'] and (i == 0 or json_string[i-1] != '\\'):
            in_string = not in_string
            result += char
            continue
            
        # Si on est dans une chaîne, on garde tout
        if in_string:
            result += char
            continue
            
        # En dehors des chaînes, on ne garde que les caractères JSON valides
        if char in '{}[],:0123456789.truefalsnul' or char.isspace():
            result += char
            
    return result


def extractTextAndImages(delta):
    """
    Extrait le texte et les images d'un objet delta Quill.

    Args:
        delta (list): Liste d'opérations Quill contenant du texte et des images

    Returns:
        tuple: (texte extrait (str), liste des URLs des images (list))
    """
    text = ""
    images = []
    
    if isinstance(delta, str):
        try:
            delta = json.loads(delta)
        except json.JSONDecodeError:
            return text, images
            
    if not isinstance(delta, list):
        if 'ops' in delta:
            delta = delta['ops']
        else:
            return text, images

    for op in delta:
        if isinstance(op, dict):
            if 'insert' in op:
                insert = op['insert']
                if isinstance(insert, str):
                    text += insert
                elif isinstance(insert, dict):
                    if 'image' in insert:
                        images.append(insert['image'])
                    elif 'formula' in insert:
                        text += insert['formula']
        elif isinstance(op, str):
            text += op

    return text.strip(), images


# Remaque : certaines bibliothèques ne sont utiles que pour cette fonction. Il serait pertinent de la rebasculer dans un autre projet à part et de virer les dépendances dans ce projet (odfpy, pandas)
def replaceElementsInText(subjectTemplate, contentTemplate, fileWithEmailsData, sheetName=0, defaultValues = []):
    
    emailTemplate = json.dumps([subjectTemplate, contentTemplate])
    
    if len(defaultValues) == 0:
        defaultValues = ['' for i in range(emailTemplate.count('{{'))]
    
    email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    datasArray = []
    _, fileExtension = os.path.splitext(fileWithEmailsData)
    print(_, fileExtension)
    if fileExtension == '.csv':
        datasArray = pandas.read_csv(fileWithEmailsData)
    elif fileExtension in ['.xls', '.xlsm', '.ods', '.xlsx']:
        extractedFromFile = pandas.read_excel(fileWithEmailsData, sheet_name=sheetName)
        datasArray = extractedFromFile.values.tolist()
    else:
        print("Unsupported file extension. Please provide a CSV, XLS, XLSX, XLSM or ODS file.")
    
    print(datasArray)
    
    emailsFields = []
    for data in datasArray:
        if not re.match(email_regex, data[0]):
            print(f"The program detected an invalid email adress: {data[0]}")
        emailText = emailTemplate
        placeholders = re.findall(r'\{\{(.*?)\}\}', emailTemplate)
        emailFields = {
            'email': data[0],
            'subject': '',
            'content': ''
        }
        for placeholder in placeholders:
            defaultValue = defaultValues[placeholders.index(placeholder)] if len(defaultValues) > placeholders.index(placeholder) else ''
            print('default value : ' + defaultValue)
            try:
                if isinstance(data[placeholders.index(placeholder) + 1], str) and data[placeholders.index(placeholder) + 1] != 'nan':
                    emailText = emailText.replace('{{' + placeholder + '}}', data[placeholders.index(placeholder) + 1])
                else:
                    raise(IndexError)
                # Si deux champs ont le même nom, ils seront tout les deux remplacés par la valeur associé au premier champ
                # S'il y a un deuxième champ, on en tiendra pas compte
            except IndexError:
                try:
                    emailText = emailText.replace('{{' + placeholder + '}}', defaultValue)
                    print(f"The placeholder {placeholder} was not found in the data file for the email {data[0]}")
                except IndexError:
                    emailText = emailText.replace('{{' + placeholder + '}}', '')
                    print(f"The placeholder {placeholder} was not found in the data file for the email {data[0]} nor in the default values. The placeholder was remplaced by an empty string.")

            print(emailText)
            
        [emailFields['subject'], emailFields['content']] = json.loads(emailText)
        emailsFields.append(emailFields)

    # print(emailsFields)
    return emailsFields

# markdownTest = "- **Liste** avec des éléments en **gras** et __soulignés__. \nVoici une formule mathématique en ligne : $E=mc^2$ et une formule mathématique en bloc : $$\int_{a}^{b} f(x) dx$$, où \( z = x + iy \), et \( u \) et \( v \) sont des fonctions réelles. Pour une matrice \( 3 \\times 3 \), le déterminant est : \n $$ \text{det}(A) = \begin{vmatrix} a & b & c \\ d & e & f \\ g & h & i \end{vmatrix} = a \begin{vmatrix} e & f \\ h & i \end{vmatrix} - b \begin{vmatrix} d & f \\ g & i \end{vmatrix} + c \begin{vmatrix} d & e \\ g & h \end{vmatrix} $$"
# print(markdownToQuilldelta(markdownTest))

# replaceElementsInText("Bonjour {{name}}, comment allez-vous ?", "Bonjour {{name}},\n\nNous vous informons que votre compte a été créé avec succès.\n\nCordialement,\nL'équipe ED-DISCE", 'usersDatas.ods')
# Tests pour la fonction closeJSON

# # Exemple de test pour vérifier le traitement des formules LaTeX dans les textes en gras
# if __name__ == "__main__":
#     # Test avec une formule LaTeX dans un texte en gras
#     test_markdown = "Ceci est un texte normal avec **une formule \[E=mc^2\] en gras** et une autre formule \[F=ma\]."
#     result = markdownToQuilldelta(test_markdown)
#     print("Test avec formule LaTeX dans texte en gras:")
#     print(json.dumps(result, indent=2))
    
#     # Test avec une formule LaTeX complexe dans un texte en gras
#     test_markdown2 = "**Voici une formule complexe: \[\int_{a}^{b} f(x) dx\] et une autre \[\frac{d}{dx}\sin(x) = \cos(x)\]**"
#     result2 = markdownToQuilldelta(test_markdown2)
#     print("\nTest avec formules LaTeX complexes dans texte en gras:")
#     print(json.dumps(result2, indent=2))
