import re
import unicodedata
from difflib import SequenceMatcher

from flask import Flask, request, jsonify

app = Flask(__name__)


CPNV_LINKS = {
    "accueil": ("Accueil CPNV", "https://www.cpnv.ch/"),
    "presentation": ("Presentation CPNV", "https://www.cpnv.ch/presentation/"),
    "formations": ("Formations", "https://www.cpnv.ch/formations/"),
    "admission": ("Admission", "https://www.cpnv.ch/admission/"),
    "inscription_es": ("Inscription ES", "https://www.cpnv.ch/admission/inscription-es/"),
    "inscriptions_em": ("Inscriptions ecole des metiers", "https://www.cpnv.ch/inscriptions-em/"),
    "inscription_preapp": ("Inscription preapprentissage", "https://www.cpnv.ch/admission/inscription-preapp/"),
    "contact": ("Contact", "https://www.cpnv.ch/contact/"),
    "sainte_landing": ("Sainte-Croix (CPNV) - site", "https://www.sainte-croix.ch/_rte/schule/7096"),
    "sainte_contenu": ("CPNV sur Sainte-Croix", "https://www.sainte-croix.ch/contenu/cpnv"),
    "stages_sc": ("Stages a Ste-Croix", "https://www.cpnv.ch/admission/stages/ste-croix-2024/"),
    "stages_info": ("Stages d'information", "https://www.cpnv.ch/admission/stages/"),
    "vivre_cpnv": ("Vivre au CPNV (documents utiles)", "https://www.cpnv.ch/vivre-au-cpnv/"),
    "intranet_accueil": ("Intranet CPNV (connexion)", "https://intranet.cpnv.ch/connexion"),
    "intranet_calendriers": ("Intranet CPNV (calendriers)", "https://intranet.cpnv.ch/calendriers"),
    "intranet_documents": ("Intranet CPNV (documents)", "https://intranet.cpnv.ch/documents"),
    "vivre_documents": ("Vivre au CPNV (documents utiles)", "https://www.cpnv.ch/vivre-au-cpnv/documents/"),
    "reglements": ("Vivre au CPNV (règlements)", "https://www.cpnv.ch/vivre-au-cpnv/reglements/"),
    "cfc_inscription": ("Inscription CFC (ecole des metiers)", "https://www.cpnv.ch/admission/inscription-cfc/"),
    "mediamaticien_cfc": ("Médiamaticien-ne CFC", "https://www.cpnv.ch/formations/technique/mediamaticien/"),
    "informaticien_cfc": ("Informaticien-ne CFC", "https://www.cpnv.ch/formations/technique/informaticien/"),
    "polymecanicien_cfc": ("Polymécanicien-ne CFC", "https://www.cpnv.ch/formations/ecole-metiers/polymecanicien/"),
    "electronicien_cfc": ("Electronicien-ne CFC", "https://www.cpnv.ch/formations/ecole-metiers/electronicien/"),
    "automaticien_cfc": ("Automaticien-ne CFC", "https://www.cpnv.ch/formations/technique/automaticien/"),
}


def _pick_cpnv_links(user_query: str):
    q = (user_query or "").lower()
    wants_admission = any(
        k in q
        for k in [
            "admission",
            "inscription",
            "inscrire",
            "candid",
            "dossier",
            "es ",
            "es/",
            "ecole des metiers",
            "metiers",
            "preapprentissage",
            "fpa",
            "bourse",
        ]
    )
    wants_stages = any(k in q for k in ["stage", "stages", "emploi", "3+1", "3+1"])
    wants_sainte = any(k in q for k in ["sainte", "sainte-croix", "ste-croix", "ste croix", "cpnv"])

    links = [CPNV_LINKS["accueil"], CPNV_LINKS["contact"], CPNV_LINKS["formations"]]
    if wants_admission:
        links = links + [CPNV_LINKS["admission"], CPNV_LINKS["inscription_es"], CPNV_LINKS["inscriptions_em"]]
        # Add one more relevant link depending on the query.
        if "preapprentissage" in q:
            links.append(CPNV_LINKS["inscription_preapp"])
    if wants_stages:
        links = links + [CPNV_LINKS["stages_info"]]
        if wants_sainte:
            links.append(CPNV_LINKS["stages_sc"])
    if wants_sainte:
        links.append(CPNV_LINKS["sainte_landing"])
        links.append(CPNV_LINKS["sainte_contenu"])

    # Deduplicate by URL, keeping order.
    seen = set()
    deduped = []
    for label, url in links:
        if url in seen:
            continue
        seen.add(url)
        deduped.append((label, url))
    return deduped[:6]  # keep it short


def _append_cpnv_links(reply: str, user_query: str):
    reply = (reply or "").rstrip()
    links = _pick_cpnv_links(user_query)
    if not links:
        return reply

    addendum_lines = ["", "Liens CPNV (officiels):"]
    for label, url in links:
        addendum_lines.append(f"- {label}: {url}")
    return reply + "\n" + "\n".join(addendum_lines)


@app.after_request
def add_no_cache_headers(resp):
    # Force le navigateur a recharger la page lors d'un refresh,
    # car l'UI est servie en HTML inline par la route `/`.
    if request.path == "/":
        resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
        resp.headers["Pragma"] = "no-cache"
        resp.headers["Expires"] = "0"
    return resp


def _strip_accents(s: str) -> str:
    s = s or ""
    s = unicodedata.normalize("NFD", s)
    return "".join(ch for ch in s if unicodedata.category(ch) != "Mn")


def _normalize_text(s: str) -> str:
    """
    Normalize french queries so we can handle typos and missing accents.
    """
    s = _strip_accents(str(s)).lower()
    # Keep letters/digits and turn everything else into spaces.
    s = re.sub(r"[^a-z0-9]+", " ", s)
    return " ".join(s.split())


def _token_fuzzy_hits(query_tokens, keyword, threshold=0.86) -> bool:
    keyword = _normalize_text(keyword)
    if not keyword:
        return False

    kw_words = keyword.split()
    if not kw_words:
        return False

    # Single word: fuzzy match token -> token.
    if len(kw_words) == 1:
        kw = kw_words[0]
        for t in query_tokens:
            if SequenceMatcher(None, t, kw).ratio() >= threshold:
                return True
        return False

    # Multi-word expression: require every word to be present (fuzzy) somewhere in the query.
    # This avoids false positives on very common first words (e.g. "comment").
    threshold_multi = max(0.78, threshold - 0.08)
    for kw_word in kw_words:
        matched = False
        for t in query_tokens:
            if SequenceMatcher(None, t, kw_word).ratio() >= threshold_multi:
                matched = True
                break
        if not matched:
            return False
    return True


def _extract_context(history) -> str:
    """
    Use the last user messages as extra context for intent detection.
    """
    parts = []
    if isinstance(history, list):
        for item in history[-6:]:
            if isinstance(item, dict) and item.get("role") == "user":
                content = item.get("content", "")
                if content:
                    parts.append(str(content))
    return " ".join(parts).strip()


def _extract_track_hint(history):
    """
    Extract a coarse track hint from the conversation.
    Used only for tailoring links/text, not for intent selection.
    """
    if not isinstance(history, list):
        return None

    texts = []
    for item in history[-10:]:
        if not isinstance(item, dict) or item.get("role") != "user":
            continue
        content = item.get("content", "")
        if content:
            texts.append(str(content))

    if not texts:
        return None

    combined = " ".join(texts)
    qnorm = _normalize_text(combined)
    tokens = qnorm.split()
    if not tokens:
        return None

    def has(tok):
        return _token_fuzzy_hits(tokens, tok, threshold=0.83)

    subtype = None
    # CFC subtypes
    if has("mediamatic") or has("media"):
        subtype = "cfc_media"
    elif has("informatic") or has("informatique") or has("it"):
        subtype = "cfc_informatique"
    elif has("polymecan") or has("mecanique"):
        subtype = "cfc_polymecanique"
    elif has("electronic") or has("electronique"):
        subtype = "cfc_electronique"
    elif has("automati") or has("automatic"):
        subtype = "cfc_automatique"

    # Main track
    if has("cfc"):
        return {"track": "cfc", "subtype": subtype}
    if has("afp"):
        return {"track": "afp", "subtype": None}
    if has("es") or has("ecole superieure"):
        return {"track": "es", "subtype": None}
    if has("fpa"):
        return {"track": "fpa", "subtype": None}

    return None


def _format_links_section(links):
    if not links:
        return ""
    lines = ["", "Liens CPNV (officiels):"]
    for label, url in links:
        lines.append(f"- {label}: {url}")
    return "\n" + "\n".join(lines)


INTENTS = [
    {
        "id": "location_sainte_croix",
        "keywords": ["sainte croix", "sainte-croix", "ste croix", "ste-croix", "cpnv", "sainte", "croix", "adresse", "gare"],
        "links": ["sainte_landing", "sainte_contenu", "contact"],
        "reply": (
            "Tu parles de Sainte-Croix (CPNV). "
            "Dis-moi ce que tu cherches exactement (inscription, stages, horaires, filiere)."
        ),
    },
    {
        "id": "greeting",
        "keywords": ["salut", "bonjour", "bonsoir", "coucou"],
        "links": [],
        "reply": (
            "Salut ! Je suis le chatbot du CPNV. "
            "Que veux-tu savoir: admission/inscription, filieres (AFP/CFC/ES), stages, ou contact/horaire ?"
        ),
    },
    {
        "id": "how_are_you",
        "keywords": ["comment ca va", "ca va", "comment tu vas", "ça va"],
        "links": [],
        "reply": (
            "Ca va bien, merci ! Et toi ? "
            "Dis-moi ta question sur le CPNV (Sainte-Croix) et je te reponds."
        ),
    },
    {
        "id": "thanks",
        "keywords": ["merci", "thx", "gratitude"],
        "links": [],
        "reply": "Avec plaisir ! Tu veux plutot des infos sur admission/inscription, ou sur les stages ?",
    },
    {
        "id": "goodbye",
        "keywords": ["au revoir", "a bientot", "bye", "ciao"],
        "links": [],
        "reply": "A bientot ! Si tu as une autre question sur le CPNV, je suis la.",
    },
    {
        "id": "timetable_cours",
        "keywords": [
            "cours",
            "x cours",
            "horaire",
            "horaires",
            "emploi du temps",
            "planning",
            "calendrier",
            "calendriers",
            "intranet",
        ],
        "links": ["intranet_calendriers", "intranet_accueil"],
        "reply": (
            "Pour voir quand tu as tes cours (planning/horaires), connecte-toi a l'intranet CPNV: "
            "https://intranet.cpnv.ch/calendriers. "
            "Cherche ensuite la periode qui t'interesse. Bon courage !"
        ),
    },
    {
        "id": "conge_absence",
        "keywords": [
            "conge",
            "demande de conge",
            "justification",
            "justifcatif",
            "justif",
            "absence",
            "absence",
            "justification d absence",
            "justificatif d absence",
        ],
        "links": ["vivre_cpnv", "intranet_documents"],
        "reply": (
            "Pour une demande de conge et/ou un justificatif d'absence:\n"
            "- Page CPNV (documents utiles): https://www.cpnv.ch/vivre-au-cpnv/\n"
            "- Intranet CPNV (documents): https://intranet.cpnv.ch"
        ),
    },
    {
        "id": "intranet_connexion",
        "keywords": ["intranet", "connexion", "se connecter", "mot de passe", "mdp", "compte", "login", "acces", "accès"],
        "links": [],
        "reply": (
            "Pour te connecter a l'intranet CPNV: https://intranet.cpnv.ch/connexion\n"
            "Si tu as un probleme de compte/mot de passe, contacte le support: helpdesk.cpnv@eduvaud.ch."
        ),
    },
    {
        "id": "reglements_charte",
        "keywords": ["reglement", "règlement", "charte", "rtt", "discipline", "sanction", "regles"],
        "links": [],
        "reply": (
            "Pour les regles officielles (reglements, memento, charte): https://www.cpnv.ch/vivre-au-cpnv/reglements/"
        ),
    },
    {
        "id": "documents_utiles",
        "keywords": ["documents utiles", "memento", "documents", "formulaires utiles", "formulaire", "formulaire utile"],
        "links": [],
        "reply": (
            "La page 'Vivre au CPNV' avec les documents utiles: https://www.cpnv.ch/vivre-au-cpnv/documents/\n"
            "Si tu me dis ce que tu cherches (conge/absence, dispense EPS, adresse, etc.), je t'oriente."
        ),
    },
    {
        "id": "vacances_jours_feries",
        "keywords": ["vacances", "vacance", "jours feries", "jours feriés", "feries", "fetes", "rattrapage"],
        "links": [],
        "reply": (
            "Pour les vacances et jours feries, regarde la rubrique 'Vivre au CPNV' du site du CPNV: "
            "https://www.cpnv.ch/vivre-au-cpnv/"
        ),
    },
    {
        "id": "contact_horaires",
        "keywords": ["contact", "telephone", "tel", "mail", "email", "adresse", "horaires", "secretariat", "secretaire"],
        "links": ["contact", "accueil"],
        "reply": (
            "Pour les coordonnées et horaires, le plus fiable est la page officielle de contact. "
            "Si tu me dis ton filiere, je peux aussi te diriger vers la bonne page."
        ),
    },
    {
        "id": "formations_overview",
        "keywords": ["formations", "formation", "filiere", "filiere", "metier", "metiers", "cfc", "afp", "es", "fpa", "maturite", "mct"],
        "links": ["formations", "presentation", "accueil", "contact"],
        "reply": (
            "Le CPNV propose plusieurs filieres et niveaux. "
            "Dis-moi: tu vises AFP/CFC, ES, FPA, ou une autre voie ?"
        ),
    },
    {
        "id": "cfc_track",
        "keywords": ["cfc", "c.f.c", "c f c"],
        "links": ["inscriptions_em", "admission", "contact", "formations"],
        "reply": (
            "OK, CFC.\n"
            "1) Choisis le metier/filiere (exemples: media/mediamaticien, informatique/informaticien, polymecanique, electronique, automatique)\n"
            "2) Suis la procedure d'inscription (delais, exigences, eventuellement entretien/examen selon la filiere)\n"
            "3) Prepare les documents demandés.\n"
            "Dis-moi: tu veux `comment s'inscrire` ou `les delais/documents` ?"
        ),
    },
    {
        "id": "cfc_media",
        "keywords": ["media", "mediamatique", "mediamaticien", "mediammatique", "mediamaticien-ne", "mediammaticien-ne"],
        "links": [],
        "reply": (
            "OK, tu parles de la filiere CFC du mediamaticien-ne.\n"
            "Infos officielles (metier): https://www.cpnv.ch/formations/technique/mediamaticien/\n"
            "Pour l'inscription CFC (ecole des metiers): https://www.cpnv.ch/admission/inscription-cfc/\n"
            "Tu veux plutot: `comment s'inscrire` ou `les delais/documents` ?"
        ),
    },
    {
        "id": "cfc_informatique",
        "keywords": ["informatique", "informaticien", "info", "it"],
        "links": [],
        "reply": (
            "OK, tu parles de la filiere CFC informaticien-ne.\n"
            "Infos officielles (metier): https://www.cpnv.ch/formations/technique/informaticien/\n"
            "Pour l'inscription CFC: https://www.cpnv.ch/admission/inscription-cfc/\n"
            "Tu veux `comment s'inscrire` ou `les delais/documents` ?"
        ),
    },
    {
        "id": "cfc_polymecanique",
        "keywords": ["polymecanique", "polymecanicien", "mecanique", "poli mecanique"],
        "links": [],
        "reply": (
            "OK, tu parles de la filiere CFC polymecanicien-ne.\n"
            "Infos officielles (metier): https://www.cpnv.ch/formations/ecole-metiers/polymecanicien/\n"
            "Pour l'inscription CFC: https://www.cpnv.ch/admission/inscription-cfc/\n"
            "Tu veux `comment s'inscrire` ou `les delais/documents` ?"
        ),
    },
    {
        "id": "cfc_electronique",
        "keywords": ["electronique", "electronicien", "electronique", "electronicien"],
        "links": [],
        "reply": (
            "OK, tu parles de la filiere CFC electronicien-ne.\n"
            "Infos officielles (metier): https://www.cpnv.ch/formations/ecole-metiers/electronicien/\n"
            "Pour l'inscription CFC: https://www.cpnv.ch/admission/inscription-cfc/\n"
            "Tu veux `comment s'inscrire` ou `les delais/documents` ?"
        ),
    },
    {
        "id": "cfc_automatique",
        "keywords": ["automatique", "automaticien", "automation"],
        "links": [],
        "reply": (
            "OK, tu parles de la filiere CFC automaticien-ne.\n"
            "Infos officielles (metier): https://www.cpnv.ch/formations/technique/automaticien/\n"
            "Pour l'inscription CFC: https://www.cpnv.ch/admission/inscription-cfc/\n"
            "Tu veux `comment s'inscrire` ou `les delais/documents` ?"
        ),
    },
    {
        "id": "afp_track",
        "keywords": ["afp"],
        "links": ["inscriptions_em", "admission", "contact", "formations"],
        "reply": (
            "OK, AFP.\n"
            "Je peux t'aider pour l'inscription: dis-moi si tu cherches `comment s'inscrire`, `les delais`, ou `les documents`."
        ),
    },
    {
        "id": "es_track",
        "keywords": ["es", "ecole superieure", "ecole superieur", "technicien", "ecole superieure"],
        "links": ["inscription_es", "admission", "formations", "contact"],
        "reply": (
            "OK, ES (ecole superieure).\n"
            "Donne-moi le domaine/filiare ES si tu sais, et dis-moi si tu veux `inscription` ou `delais/examen`."
        ),
    },
    {
        "id": "fpa_track",
        "keywords": ["fpa"],
        "links": ["admission", "formations", "contact"],
        "reply": (
            "OK, FPA.\n"
            "Les etapes depend de ton profil. Dis-moi si tu as une maturite gymnasiale ou un certificat de culture generale."
        ),
    },
    {
        "id": "admission_general",
        "keywords": [
            "admission",
            "admis",
            "admettre",
            "entrer",
            "entree",
            "inscription",
            "inscrire",
            "candidature",
            "candidat",
            "dossier",
            "documents",
            "document",
            "pieces",
            "piece",
            "delais",
            "dates",
            "date",
        ],
        "links": ["admission", "formations", "contact", "accueil"],
        "reply": (
            "Pour etre admis au CPNV, la procedure depend de ta voie (AFP/CFC, ES, preapprentissage, FPA). "
            "En general, il faut suivre les etapes et delais indiqués sur la page officielle. "
            "Dis-moi ton niveau et ton objectif, et je te guide vers la bonne etape."
        ),
    },
    {
        "id": "inscription_es",
        "keywords": ["es", "ecole superieure", "ecole superieur", "technicien", "inscription es", "inscrire es"],
        "links": ["inscription_es", "admission", "formations", "contact"],
        "reply": (
            "Inscription en Ecole Superieure (ES): "
            "les etapes et delais exacts sont sur la page officielle. "
            "Indique-moi la filiere ES que tu vises (si tu sais)."
        ),
    },
    {
        "id": "inscriptions_ecole_metiers",
        "keywords": ["ecole des metiers", "ecole metiers", "metiers", "inscriptions", "afp", "cfc"],
        "links": ["inscriptions_em", "admission", "formations", "contact"],
        "reply": (
            "Inscription pour l'ecole des metiers: "
            "les conditions et inscriptions se trouvent sur la page officielle. "
            "Si tu me dis ton metier/filiere (ex: informatique, polymecanique, mediammatique), je t'oriente."
        ),
    },
    {
        "id": "inscription_preapprentissage",
        "keywords": ["preapprentissage", "preapp", "pre-apprentissage", "pre app"],
        "links": ["inscription_preapp", "admission", "contact"],
        "reply": (
            "Pour le preapprentissage (etapes d'inscription): "
            "consulte la page officielle. "
            "Dis-moi aussi d'ou tu viens (transition/sem o autre) si tu sais."
        ),
    },
    {
        "id": "stages",
        "keywords": ["stage", "stages", "3 plus 1", "3+1", "information", "emploi"],
        "links": ["stages_info", "stages_sc", "sainte_landing", "contact"],
        "reply": (
            "Pour les stages (information et delais), regarde la page officielle. "
            "Si tu cherches un stage a Sainte-Croix, precise la periode/annee."
        ),
    },
    {
        "id": "fpa",
        "keywords": ["fpa", "formation", "acceleree", "acceleree", "accélérée", "accelerer"],
        "links": ["admission", "formations", "contact"],
        "reply": (
            "La FPA (formation professionnelle acceleree) depend de ton profil. "
            "Dis-moi si tu as une maturite gymnasiale ou un certificat de culture generale, et je te pointe vers l'etape officielle."
        ),
    },
    {
        "id": "capabilities",
        "keywords": [
            "a quoi tu sers",
            "a quoi tu sers",
            "a quoi tu peux repondre",
            "a quoi tu peux repondre",
            "quelles questions",
            "quelles questions je peux te poser",
            "que peux tu",
            "que peux tu repondre",
            "ce que tu sais",
            "tu sais faire",
            "utilite",
        ],
        "links": [],
        "reply": (
            "Je peux t'aider sur tout ce qui concerne le CPNV (Sainte-Croix) :\n"
            "- Admission/inscription (AFP/CFC, ES, preapprentissage, FPA)\n"
            "- Filières CFC (ex: media -> mediamaticien-ne, informatique -> informaticien-ne, etc.)\n"
            "- Stages (information et stages a Sainte-Croix quand c'est pertinent)\n"
            "- Horaires/cours : je te guide vers l'intranet (calendriers)\n"
            "- Congé / justification d'absence\n"
            "- Documents utiles (formulaires) + reglements/charte/memento\n"
            "- Vacances et jours feries (ou ou trouver l'info)\n"
            "- Contact (ou trouver l'adresse/telephone)\n\n"
            "Pose-moi ta question, meme si tu fais des fautes d'orthographe. "
            "Si tu veux, dis aussi ta voie (AFP/CFC/ES) pour que ce soit plus precis."
        ),
    },
    {
        "id": "default",
        "keywords": [],
        "links": ["admission", "formations", "contact", "sainte_landing"],
        "reply": (
            "Je peux t'aider sur le CPNV (Sainte-Croix): admission/inscription (AFP/CFC/ES/FPA), stages, horaires/cours (intranet), conge/absence, documents et contact.\n"
            "Dis-moi ta question (meme avec des fautes) et si possible: ta voie (AFP/CFC/ES) et ton besoin (inscription, delais, documents, stages...)."
        ),
    },
]


def _pick_intent(user_msg: str, history):
    # Track override based ONLY on what the user just typed (not on history context).
    current_qnorm = _normalize_text(user_msg)
    current_tokens = current_qnorm.split()
    if len(current_tokens) == 1:
        single = current_tokens[0]
        track_map = {"cfc": "cfc_track", "afp": "afp_track", "es": "es_track", "fpa": "fpa_track"}
        track_id = track_map.get(single)
        if track_id:
            for intent in INTENTS:
                if intent.get("id") == track_id:
                    return intent

    # Intent detection should rely on what the user wrote *now*.
    # Using history here can cause irrelevant matches and make the dialogue incoherent.
    qnorm = _normalize_text(user_msg)
    qtokens = qnorm.split()

    best = None
    best_score = 0

    for intent in INTENTS:
        if intent["id"] == "default":
            continue
        keywords = intent.get("keywords", [])
        if not keywords:
            continue

        hits = 0
        for kw in keywords:
            if _token_fuzzy_hits(qtokens, kw):
                hits += 1

        # Strong boost for multi-keyword intents.
        score = hits
        if score > best_score:
            best_score = score
            best = intent

    if best is None or best_score <= 0:
        return next(i for i in INTENTS if i["id"] == "default")
    return best


def _generate_reply(user_msg, history):
    user_msg = (user_msg or "").strip()
    if not user_msg:
        return "Ecris un message pour commencer."

    intent = _pick_intent(user_msg=user_msg, history=history)

    track_hint = _extract_track_hint(history)

    # Greeting coherence: only if the user included a greeting in THIS message.
    prefix_salutation = ""
    if intent.get("id") not in {"greeting", "how_are_you"}:
        current_tokens = set(_normalize_text(user_msg).split())
        if current_tokens.intersection({"salut", "bonjour", "bonsoir", "coucou"}):
            prefix_salutation = "Salut ! "

    def wants_links():
        # Keep the chat clean: only show official links when the user likely wants detailed info.
        qnorm = _normalize_text(user_msg)
        tokens = set(qnorm.split())

        # For these intents, we already include the useful URLs directly in the reply text.
        # So we avoid the extra "Liens CPNV" section.
        if intent.get("id") in {"timetable_cours", "conge_absence", "cfc_media", "cfc_informatique", "cfc_polymecanique", "cfc_electronique", "cfc_automatique"}:
            return False

        if intent.get("id") in {"timetable_cours", "conge_absence", "intranet_connexion", "reglements_charte", "documents_utiles", "vacances_jours_feries"}:
            return False

        always_link_for = {
            "location_sainte_croix",
            "contact_horaires",
            "stages",
            "inscription_es",
            "inscriptions_ecole_metiers",
            "inscription_preapprentissage",
            "admission_general",
            "timetable_cours",
            "conge_absence",
        }

        if intent.get("id") not in always_link_for and intent.get("id") not in {"cfc_track", "afp_track", "es_track", "fpa_track"}:
            return False

        # Explicit need for details.
        detail_tokens = {
            "lien",
            "liens",
            "renseigner",
            "renseignement",
            "officiel",
            "adresse",
            "horaires",
            "contact",
            "stage",
            "stages",
            "inscription",
            "inscrire",
            "admis",
            "admettre",
            "documents",
            "document",
            "piece",
            "pieces",
            "dossier",
            "delais",
            "date",
            "dates",
            "examen",
            "examens",
            "entree",
            "entrer",
            "periode",
            "periodes",
            "conge",
            "absence",
        }

        if tokens.intersection(detail_tokens):
            return True

        # If user asks "comment ..." about a track, they usually want the steps + link.
        if "comment" in tokens and tokens.intersection({"inscrire", "admis", "admettre", "entree", "entrer", "stage", "stages"}):
            return True

        # Track-only answers like "CFC" should not trigger link spam.
        if intent.get("id") in {"cfc_track", "afp_track", "es_track", "fpa_track"} and tokens.issubset({"cfc", "afp", "es", "fpa"}):
            return False

        # Default: no links.
        return False

    want_links = wants_links()
    links = []
    if want_links:
        # Tailor links for admission/docs/delais when track is known.
        if intent.get("id") == "admission_general" and track_hint and track_hint.get("track"):
            t = track_hint.get("track")
            subtype = track_hint.get("subtype")
            if t == "cfc":
                links_to_add = ["cfc_inscription", "contact"]
                subtype_key_to_link = {
                    "cfc_media": "mediamaticien_cfc",
                    "cfc_informatique": "informaticien_cfc",
                    "cfc_polymecanique": "polymecanicien_cfc",
                    "cfc_electronique": "electronicien_cfc",
                    "cfc_automatique": "automaticien_cfc",
                }
                if subtype in subtype_key_to_link:
                    links_to_add.insert(1, subtype_key_to_link[subtype])
                for key in links_to_add:
                    pair = CPNV_LINKS.get(key)
                    if pair:
                        links.append(pair)
            elif t == "es":
                for key in ["inscription_es", "admission", "contact"]:
                    pair = CPNV_LINKS.get(key)
                    if pair:
                        links.append(pair)
            elif t == "afp":
                for key in ["inscriptions_em", "admission", "contact"]:
                    pair = CPNV_LINKS.get(key)
                    if pair:
                        links.append(pair)
            elif t == "fpa":
                for key in ["admission", "formations", "contact"]:
                    pair = CPNV_LINKS.get(key)
                    if pair:
                        links.append(pair)
        else:
            for key in intent.get("links", []):
                pair = CPNV_LINKS.get(key)
                if pair:
                    links.append(pair)

    reply = (intent.get("reply") or "").strip()

    # Small "dynamic" tailoring inside the same intent.
    # This helps for follow-ups like: "comment etre admis ?" -> "et les documents ?"
    qnorm = _normalize_text(user_msg)
    qtokens = set(qnorm.split())

    if intent.get("id") == "admission_general":
        if qtokens.intersection({"document", "documents", "piece", "pieces", "dossier", "cv", "attestation", "certificat"}):
            reply = (
                "Pour les documents/dossier a fournir, cela depend de ta voie (AFP/CFC, ES, preapprentissage, FPA). "
                "Verifie la liste exacte sur la page officielle d'admission/inscription, et si tu as un doute (niveau/filiere), "
                "dis-moi laquelle tu vises et je t'oriente vers la bonne etape."
            )
        elif qtokens.intersection({"delais", "date", "dates", "deadline"}):
            reply = (
                "Les delais et dates exacts dependent de la voie (AFP/CFC, ES, preapprentissage, FPA). "
                "Consulte la page officielle d'admission/inscription: tu y trouves les periodes, examens et inscriptions."
            )

    final_reply = prefix_salutation + reply if prefix_salutation else reply
    return final_reply + (_format_links_section(links) if want_links else "")


@app.route("/chat", methods=["POST"])
def chat():
    data = request.get_json(silent=True) or {}
    user_msg = data.get("message", "")
    history = data.get("history", [])

    reply = _generate_reply(user_msg=user_msg, history=history)
    return jsonify({"reply": reply})

@app.route("/")
def home():
    return """
<!doctype html>
<html lang="fr">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Chatbot</title>
    <style>
      :root {
        --bg1: #0f172a;
        --bg2: #111827;
        --card: rgba(255,255,255,0.92);
        --text: #0f172a;
        --muted: #64748b;
        --user: #2563eb;
        --bot: #f1f5f9;
      }

      * { box-sizing: border-box; }

      body {
        margin: 0;
        font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
        color: var(--text);
        background: radial-gradient(1200px circle at 20% 10%, #1d4ed8 0%, transparent 35%),
                    radial-gradient(1000px circle at 70% 30%, #22c55e 0%, transparent 40%),
                    linear-gradient(180deg, var(--bg1), var(--bg2));
        min-height: 100vh;
        display: flex;
        align-items: flex-start;
        justify-content: center;
      }

      .container {
        width: 100%;
        max-width: 980px;
        padding: 22px;
      }

      .header {
        display: flex;
        gap: 14px;
        align-items: center;
        margin-bottom: 14px;
        color: #e5e7eb;
      }

      .badge {
        background: rgba(255,255,255,0.12);
        border: 1px solid rgba(255,255,255,0.18);
        padding: 6px 10px;
        border-radius: 999px;
        font-size: 12px;
        color: #e5e7eb;
      }

      .card {
        background: var(--card);
        border-radius: 18px;
        box-shadow: 0 10px 30px rgba(0,0,0,0.25);
        overflow: hidden;
        border: 1px solid rgba(15,23,42,0.08);
        backdrop-filter: blur(8px);
      }

      .chat {
        height: 70vh;
        min-height: 420px;
        padding: 16px;
        overflow-y: auto;
        display: flex;
        flex-direction: column;
        gap: 10px;
        background:
          linear-gradient(180deg, rgba(37,99,235,0.06), transparent 40%),
          linear-gradient(0deg, rgba(34,197,94,0.05), transparent 45%);
      }

      .msg {
        display: flex;
      }
      .msg.user { justify-content: flex-end; }
      .msg.bot { justify-content: flex-start; }

      .bubble {
        max-width: 78%;
        padding: 10px 12px;
        border-radius: 16px;
        line-height: 1.4;
        white-space: pre-wrap;
        word-break: break-word;
        border: 1px solid rgba(15,23,42,0.08);
      }
      .bubble.user {
        background: var(--user);
        color: white;
        border-bottom-right-radius: 6px;
        border-color: rgba(37,99,235,0.3);
      }
      .bubble.bot {
        background: var(--bot);
        color: #0f172a;
        border-bottom-left-radius: 6px;
      }

      .meta {
        font-size: 11px;
        color: rgba(255,255,255,0.85);
        margin-bottom: 6px;
        font-weight: 500;
      }
      .bubble.bot .meta {
        color: rgba(15,23,42,0.55);
      }

      .composer {
        padding: 14px;
        border-top: 1px solid rgba(15,23,42,0.08);
        display: flex;
        gap: 10px;
        align-items: flex-end;
        background: rgba(255,255,255,0.7);
      }

      textarea {
        width: 100%;
        resize: none;
        min-height: 46px;
        max-height: 140px;
        padding: 10px 12px;
        border-radius: 12px;
        border: 1px solid rgba(15,23,42,0.14);
        outline: none;
        font-size: 14px;
        background: rgba(255,255,255,0.95);
      }
      textarea:focus {
        border-color: rgba(37,99,235,0.55);
        box-shadow: 0 0 0 4px rgba(37,99,235,0.15);
      }

      .actions {
        display: flex;
        flex-direction: column;
        gap: 8px;
        align-items: stretch;
        min-width: 140px;
      }

      button {
        border: 0;
        border-radius: 12px;
        padding: 11px 12px;
        font-weight: 700;
        cursor: pointer;
        background: rgba(37,99,235,0.95);
        color: white;
        box-shadow: 0 6px 18px rgba(37,99,235,0.28);
      }
      button:disabled {
        cursor: not-allowed;
        opacity: 0.6;
        box-shadow: none;
      }

      .hint {
        font-size: 12px;
        color: var(--muted);
        line-height: 1.2;
      }

      .typing {
        display: inline-flex;
        gap: 6px;
        align-items: center;
      }
      .dot {
        width: 6px;
        height: 6px;
        border-radius: 50%;
        background: currentColor;
        opacity: 0.55;
        animation: pulse 1.1s infinite ease-in-out;
      }
      .dot:nth-child(2) { animation-delay: 0.12s; }
      .dot:nth-child(3) { animation-delay: 0.24s; }

      @keyframes pulse {
        0% { transform: translateY(0); opacity: 0.35; }
        50% { transform: translateY(-3px); opacity: 0.95; }
        100% { transform: translateY(0); opacity: 0.35; }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="header">
        <div>
          <div style="font-size: 22px; font-weight: 850;">Chatbot</div>
          <div style="font-size: 13px; color: #cbd5e1;">Tape un message pour commencer</div>
        </div>
        <div class="badge">UI simple + historique</div>
      </div>

      <div class="card">
        <div id="chat" class="chat"></div>

        <div class="composer">
          <textarea id="msg" placeholder="Ecris ton message... (Enter pour envoyer, Shift+Enter pour retour a la ligne)"></textarea>
          <div class="actions">
            <button id="send" type="button">Envoyer</button>
            <div id="status" class="hint"></div>
          </div>
        </div>
      </div>
    </div>

    <script>
      const chat = document.getElementById('chat');
      const msg = document.getElementById('msg');
      const send = document.getElementById('send');
      const statusEl = document.getElementById('status');

      // Historique cote client (le serveur ne garde que le dernier N).
      const history = [];

      function formatTime(d) {
        const hh = String(d.getHours()).padStart(2, '0');
        const mm = String(d.getMinutes()).padStart(2, '0');
        return hh + ':' + mm;
      }

      function escapeHtml(s) {
        return String(s).replace(/[&<>"']/g, function (m) {
          return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]);
        });
      }

      function linkify(text) {
        // Turns URLs into clickable anchors while keeping other text safe.
        const frag = document.createDocumentFragment();
        const urlRegex = /https?:\/\/[^\s<]+/g;
        let lastIndex = 0;
        let match;

        while ((match = urlRegex.exec(text)) !== null) {
          const before = text.slice(lastIndex, match.index);
          if (before) {
            const span = document.createElement('span');
            span.textContent = before;
            frag.appendChild(span);
          }

          const url = match[0];
          const a = document.createElement('a');
          a.href = url;
          a.textContent = url;
          a.target = '_blank';
          a.rel = 'noopener noreferrer';
          a.style.wordBreak = 'break-all';
          frag.appendChild(a);

          lastIndex = match.index + url.length;
        }

        const remaining = text.slice(lastIndex);
        if (remaining) {
          const tail = document.createElement('span');
          tail.textContent = remaining;
          frag.appendChild(tail);
        }

        return frag;
      }

      function appendMessage(role, text) {
        const wrapper = document.createElement('div');
        wrapper.className = 'msg ' + (role === 'user' ? 'user' : 'bot');

        const bubble = document.createElement('div');
        bubble.className = 'bubble ' + (role === 'user' ? 'user' : 'bot');

        const who = role === 'user' ? 'Vous' : 'Bot';
        const meta = document.createElement('div');
        meta.className = 'meta';
        meta.textContent = who + ' • ' + formatTime(new Date());
        bubble.appendChild(meta);

        const content = document.createElement('div');
        content.appendChild(linkify(String(text)));
        bubble.appendChild(content);

        wrapper.appendChild(bubble);
        chat.appendChild(wrapper);
        chat.scrollTop = chat.scrollHeight;
        return wrapper;
      }

      function setLoading(isLoading) {
        send.disabled = isLoading;
        statusEl.textContent = isLoading ? 'En train de repondre...' : '';
      }

      function createTypingPlaceholder() {
        const wrapper = document.createElement('div');
        wrapper.className = 'msg bot';

        const bubble = document.createElement('div');
        bubble.className = 'bubble bot';
        bubble.innerHTML =
          '<div class="meta">Bot • ' + formatTime(new Date()) + '</div>' +
          '<div class="typing"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>';

        wrapper.appendChild(bubble);
        chat.appendChild(wrapper);
        chat.scrollTop = chat.scrollHeight;
        return wrapper;
      }

      async function sendMsg() {
        const text = (msg.value || '').trimEnd();
        if (!text) return;

        appendMessage('user', text);
        history.push({ role: 'user', content: text });

        msg.value = '';
        msg.focus();

        setLoading(true);
        const typingEl = createTypingPlaceholder();

        try {
          const res = await fetch('/chat', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ message: text, history })
          });

          const data = await res.json().catch(() => ({}));
          const reply = (data && data.reply) ? data.reply : '';

          chat.removeChild(typingEl);
          appendMessage('assistant', reply || '(reponse vide)');
          history.push({ role: 'assistant', content: reply || '' });
        } catch (e) {
          chat.removeChild(typingEl);
          appendMessage('assistant', 'Erreur de connexion.');
        } finally {
          setLoading(false);
        }
      }

      send.addEventListener('click', sendMsg);
      msg.addEventListener('keydown', (e) => {
        // Enter pour envoyer, Shift+Enter pour inserer une nouvelle ligne.
        if (e.key === 'Enter' && !e.shiftKey) {
          e.preventDefault();
          sendMsg();
        }
      });
    </script>
  </body>
</html>
    """

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
