from flask import Flask, request, jsonify
import re
import unicodedata
from functools import lru_cache
import time

app = Flask(__name__)
from flask_cors import CORS
CORS(app)  # Autorise toutes les origines (pour test)

import logging
import os

# Configure le logging dans un fichier à côté de app.py
LOG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'nlp_api.log')
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s %(levelname)s %(message)s',
    handlers=[logging.FileHandler(LOG_PATH, encoding='utf-8'), logging.StreamHandler()]
)
log = logging.getLogger('nlp_api')


import json
import os
import requests
from apscheduler.schedulers.background import BackgroundScheduler
from pytz import timezone

LABOS_JSON_URL = "https://vence-pharmacieposte.fr/modules/ajaxsmartsearch/export_labos.php"

# Déclenche la génération du fichier labos.json côté PrestaShop
def update_labos_json():
    try:
        resp = requests.get(LABOS_JSON_URL, timeout=30)
        if resp.status_code == 200:
            print(f"[NLP DEBUG] Mise à jour labos.json côté PrestaShop : OK ({resp.json()['count']} labos)")
        else:
            print(f"[NLP DEBUG] Erreur lors de la mise à jour labos.json : {resp.status_code}")
    except Exception as e:
        print(f"[NLP DEBUG] Exception lors de la mise à jour labos.json : {e}")

def schedule_labos_update():
    scheduler = BackgroundScheduler(timezone=timezone('Europe/Paris'))
    # Tous les jours à minuit heure française
    scheduler.add_job(update_labos_json, 'cron', hour=0, minute=0)
    scheduler.start()

# Appelle cette fonction au démarrage du service Python
schedule_labos_update()

def load_labos():
    # Chemin à adapter selon l'endroit où PrestaShop génère le fichier
    path = os.path.join(os.path.dirname(__file__), 'labos.json')
    try:
        with open(path, 'r', encoding='utf-8') as f:
            return [l.lower() for l in json.load(f)]
    except Exception as e:
        print(f"[NLP DEBUG] Impossible de charger la liste des labos: {e}")
        return []

def normalize(text):
    # Supprime les accents et met en minuscule
    return ''.join(
        c for c in unicodedata.normalize('NFD', text)
        if unicodedata.category(c) != 'Mn'
    ).lower()

# Décorer la fonction parse_query avec un cache en mémoire
@lru_cache(maxsize=1000)
# Décorer la fonction parse_query avec un cache en mémoire
@lru_cache(maxsize=1000)
def parse_query(query):
    # Optimisation : normalisation préalable de la requête
    query = query.lower().strip()
    if not query or len(query) < 2:
        return None
        
    price_min, price_max, labo = None, None, None
    promo_only = False  # Nouveau flag pour les promotions
    
    # Chargement optimisé des laboratoires avec cache
    labos_dyn = load_labos()
    
    # Création du mapping normalisé -> labo original (avec cache)
    static_normalized_labos = getattr(parse_query, '_normalized_labos', None)
    if static_normalized_labos is None or len(static_normalized_labos) != len(labos_dyn):
        normalized_labos = {normalize(l): l for l in labos_dyn}
        parse_query._normalized_labos = normalized_labos
    else:
        normalized_labos = static_normalized_labos
    
    # Analyse optimisée des tokens
    tokens = query.split()
    labo = None
    name_tokens = []
    
    # Utilisation d'un set pour la recherche de laboratoires (plus rapide)
    normalized_tokens = {normalize(token) for token in tokens}
    
    # Recherche labo : correspondance exacte uniquement (pas de fuzzy, pas de NLP sur labo)
    labo = None
    query_norm = normalize(query)
    if query_norm in normalized_labos:
        labo = normalized_labos[query_norm]
        name_tokens = []
    else:
        name_tokens = tokens
    # Compilation préalable des expressions régulières (optimisation)
    if not hasattr(parse_query, '_regex_cache'):
        parse_query._regex_cache = {
            'price_max': [
                re.compile(r'(moins de|inférieur à|<)\s*([0-9]+)\s*(?:€|euros?)?'),
                re.compile(r'moins de\s*([0-9]+)\s*(?:€|euros?)?'),
                re.compile(r'([0-9]+)\s*(?:€|euros?)?\s*maximum')
            ],
            'price_min': [
                re.compile(r'(plus de|supérieur à|>)\s*([0-9]+)\s*(?:€|euros?)?'),
                re.compile(r'plus de\s*([0-9]+)\s*(?:€|euros?)?'),
                re.compile(r'([0-9]+)\s*(?:€|euros?)?\s*minimum')
            ],
            'price_exact': [
                re.compile(r'(?:prix|coûte|vaut)\s*([0-9]+)\s*(?:€|euros?)?'),
                re.compile(r'\b([0-9]+)\s*(?:€|euros?)\b')
            ],
            'promo': [
                re.compile(r'\b(?:promo|promotion|promotions)\b'),
                re.compile(r'\b(?:solde|soldes)\b'),
                re.compile(r'\b(?:réduction|réductions)\b'),
                re.compile(r'\b(?:remise|remises)\b'),
                re.compile(r'\ben promotion\b')
            ]
        }
    
    # Extraction prix max avec regex pré-compilées
    for regex in parse_query._regex_cache['price_max']:
        m = regex.search(query)
        if m:
            price_max = float(m.group(2) if len(m.groups()) > 1 else m.group(1))
            break
    
    # Extraction prix min avec regex pré-compilées
    for regex in parse_query._regex_cache['price_min']:
        m = regex.search(query)
        if m:
            price_min = float(m.group(2) if len(m.groups()) > 1 else m.group(1))
            break
            
    # Extraction prix exact (convertir en min et max identiques)
    if price_min is None and price_max is None:
        for regex in parse_query._regex_cache['price_exact']:
            m = regex.search(query)
            if m:
                exact_price = float(m.group(1))
                price_min = exact_price
                price_max = exact_price
                break
                
    # Détection des promotions
    for regex in parse_query._regex_cache['promo']:
        if regex.search(query):
            promo_only = True
            # Si la requête ne contient que "promo" ou "promotions", supprimer le nom pour éviter les faux positifs
            if len(query.strip().split()) <= 2 and not labo and not price_min and not price_max:
                name = ''
            break
    
    # Construction du nom à partir des tokens filtrés
    name = ' '.join(name_tokens).strip()
    # Nettoyage des stopwords et verbes courants avec regex pré-compilées
    if not hasattr(parse_query, '_stopwords_regex'):
        stopwords = [
            r'\bje veux\b', r'\bje cherche\b', r'\btrouver\b', r'\btrouvez\b', r'\bcherche\b', r'\brecherche\b',
            r'\bj\'aimerais\b', r'\bj\'aimerai\b', r'\bmontre\b', r'\bmontrez\b', r'\baffiche\b', r'\bafficher\b',
            r'\bmontre moi\b', r'\bmontrez moi\b', r'\baffiche moi\b', r'\bafficher moi\b',
            r'\bdu\b', r'\bde la\b', r'\bdes\b', r'\ble\b', r'\bla\b', r'\bl\''
        ]
        # Combiner tous les stopwords en une seule expression régulière
        parse_query._stopwords_regex = re.compile('|'.join(stopwords))
    
    # Appliquer le nettoyage en une seule opération
    query_clean = parse_query._stopwords_regex.sub('', query).strip()

    # Compilation préalable des expressions régulières pour l'extraction du nom
    if not hasattr(parse_query, '_name_regex'):
        parse_query._name_regex = {
            'price_indicators': re.compile(r'(moins de|inférieur à|plus de|supérieur à|entre|et|[0-9]+\s*€?|euros?)'),
            'connectors': re.compile(r'\b(à|de|chez|pour|au prix de|à partir de|à moins de|à plus de|€|euros?)\b')
        }
    
    # Extraire le nom AVANT de retirer la partie prix (avec regex pré-compilées)
    name_parts = parse_query._name_regex['price_indicators'].split(query_clean, 1)
    name = name_parts[0].strip() if name_parts else ''
    
    # Nettoyer le nom extrait des connecteurs (avec regex pré-compilées)
    if name:
        name_parts = parse_query._name_regex['connectors'].split(name, 1)
        name = name_parts[0].strip() if name_parts else name

    # Fallback optimisé : si aucun nom extrait, prendre le dernier mot
    if not name:
        words = query_clean.strip().split()
        if words:
            name = words[-1]

    # Si le nom est trop court, le vider pour éviter une recherche inutile sur "du", "le", etc.
    if len(name) < 3:
        name = ''
    print(f"[NLP DEBUG] Résultat NLP: name='{name}', labo='{labo}', price_min={price_min}, price_max={price_max}")
    # Retourne un résultat si au moins name, labo ou promo est présent
    if not name and not labo and not promo_only:
        return None
    if labo:
        print(f"[NLP DEBUG] labo détecté: {labo}")
    return {
        'name': name,
        'labo': labo,
        'price_min': price_min,
        'price_max': price_max,
        'promo_only': promo_only
    }

@app.route('/')
def index():
    return 'API NLP OK!'

@app.route('/parse', methods=['POST'])
def parse():
    try:
        log.info('--- Appel /parse ---')
        # Vérifier l'authentification
        auth_header = request.headers.get('X-Api-Secret')
        if not auth_header or auth_header != '2QN631jHzUQwO1ohpionmt1TAfpchkNbJtCy7lDEwbw=':
            log.warning('Accès refusé : clé API incorrecte')
            return jsonify({'error': 'Unauthorized access'}), 401
        data = request.get_json(force=True, silent=True)
        log.debug(f"Payload reçu : {data}")
        if not data or 'query' not in data:
            log.error('Requête invalide ou champ "query" manquant')
            return jsonify({'error': 'Invalid request'}), 400
        query = data['query']
        # Traitement NLP réel
        import unicodedata
        def normalize(txt):
            if not isinstance(txt, str):
                return ''
            return unicodedata.normalize('NFKD', txt).encode('ASCII', 'ignore').decode('utf-8').lower().strip()

        try:
            with open('labos.json', encoding='utf-8') as f:
                labos = json.load(f)
            log.info(f"labos.json chargé : {len(labos)} labos")
        except Exception as e:
            log.error(f"Erreur lors de la lecture de labos.json : {e}")
            return jsonify({'error': 'labos.json introuvable ou invalide', 'detail': str(e)}), 500

        query_norm = normalize(query)
        log.debug(f"Query normalisée : {query_norm}")
        labo_trouve = None
        for labo in labos:
            if isinstance(labo, dict):
                nom_labo = labo.get('name') or labo.get('nom') or ''
            elif isinstance(labo, str):
                nom_labo = labo
            else:
                nom_labo = ''
            if normalize(nom_labo) == query_norm:
                labo_trouve = nom_labo
                log.info(f"Labo détecté : {labo_trouve}")
                break
        if not labo_trouve:
            log.info("Aucun labo détecté pour cette query")
        return jsonify({'labo': labo_trouve, 'query': query})
    except Exception as e:
        log.exception(f"Exception non gérée dans /parse : {e}")
        return jsonify({'error': 'Internal server error', 'detail': str(e)}), 500

@app.route('/labos', methods=['GET', 'POST'])
def get_labos():
    auth_header = request.headers.get('X-Api-Secret')
    if not auth_header or auth_header != '2QN631jHzUQwO1ohpionmt1TAfpchkNbJtCy7lDEwbw=':
        return jsonify({'error': 'Unauthorized access'}), 401
    with open('labos.json', encoding='utf-8') as f:
        data = json.load(f)
    return jsonify(data)
    
    # Récupérer et traiter la requête
    start_time = time.time()
    data = request.get_json()
    query = data.get('query', '')
    
    # Vérification rapide pour éviter le traitement complet
    if not query or len(query.strip()) < 3:
        return jsonify(None)
    
    # Analyser la requête NLP classique
    result = parse_query(query)
    
    # Recherche mixte de labos pertinents (matching partiel/fuzzy)
    def find_matching_labos(query, labos):
        qnorm = normalize(query)
        tokens = qnorm.split()
        matches = set()
        for labo in labos:
            ln = normalize(labo)
            for token in tokens:
                if token and (token in ln or ln in token):
                    matches.add(labo)
        print(f"[NLP DEBUG] Tokens utilisés pour matching labo : {tokens}")
    print(f"[NLP DEBUG] Labos matchés : {sorted(matches)}")
    return sorted(matches)
    
    labos_list = load_labos()
    labos_matches = find_matching_labos(query, labos_list)
    
    # Enregistrer le temps de traitement pour le débogage
    processing_time = time.time() - start_time
    print(f"[NLP DEBUG] Requête traitée en {processing_time:.4f} secondes: '{query}'")
    if result:
        print(f"[NLP DEBUG] Résultat: name='{result.get('name', '')}', labo='{result.get('labo', '')}', "
              f"price_min={result.get('price_min')}, price_max={result.get('price_max')}")
    print(f"[NLP DEBUG] Labos détectés (mixte): {labos_matches}")
    
    # Ajoute la liste des labos détectés à la réponse
    if result is None:
        result = {}
    result['labos_matches'] = labos_matches
    
    # Log de synthèse compact
    print(f"[NLP SYNTH] Query='{query}' | name='{result.get('name','') if result else ''}' | labo='{result.get('labo','') if result else ''}' | prix=[{result.get('price_min','') if result else ''},{result.get('price_max','') if result else ''}] | promo={result.get('promo_only','') if result else ''} | labos_matches={labos_matches} | t={processing_time:.3f}s")
    print("[NLP DEBUG] JSON retourné:", json.dumps(result))
    return jsonify(result)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)