"""Helpers agnostiques du moteur : extraction JSON tolerante + retrait de la
chaine de pensee des modeles a raisonnement.

Module neutre (aucune dependance a un backend) : partage par la facade `LLM` et
par les backends (qui s'en servent pour l'arret anticipe en streaming), sans
creer de cycle d'import.
"""
from __future__ import annotations

import json
import re
from typing import Any

# Bornes d'un bloc JSON dans une reponse potentiellement bavarde.
_JSON_SPAN_RE = re.compile(r"(\{.*\}|\[.*\])", re.DOTALL)
_FENCE_RE = re.compile(r"```(?:json)?\s*(.*?)```", re.DOTALL)

# Marqueurs de FIN de chaine de pensee : on ne garde que ce qui suit le dernier.
# - balises type DeepSeek-R1 / Qwen-think
# - format a canaux type Gemma 4 / Harmony (la pensee est close par <channel|>)
_REASONING_END_MARKERS = ("</think>", "<channel|>", "<|channel|>")
# Prefixe de canal/think non ferme reste en tete (pensee tronquee) : a retirer.
_REASONING_OPEN_RE = re.compile(r"^\s*(?:<\|?channel\|?>\s*\w*|<think>)", re.IGNORECASE)


def _strip_reasoning(text: str) -> str:
    """Retire la chaine de pensee d'un modele a raisonnement.

    Ne conserve que ce qui suit le dernier marqueur de fin de pensee
    (`</think>`, `<channel|>`...). Si seul un marqueur d'ouverture non ferme
    subsiste (pensee tronquee par le budget de tokens), on le retire en tete
    pour eviter de parser la pensee a la place de la reponse.
    """
    t = text
    for marker in _REASONING_END_MARKERS:
        if marker in t:
            t = t.rsplit(marker, 1)[-1]
    t = _REASONING_OPEN_RE.sub("", t)
    return t.strip()


def _has_complete_json(text: str) -> bool:
    """True si `text` contient deja un objet/array JSON complet et parsable.

    Sert a stopper la generation des modeles a raisonnement des que la reponse
    finale est ecrite (evite de consumer le budget en boucles de pensee).
    """
    try:
        _extract_json(text)
        return True
    except Exception:  # noqa: BLE001
        return False


def _extract_json(text: str) -> Any:
    """Extrait le premier objet/array JSON d'une reponse libre du modele.

    Tolere le texte parasite avant/apres (y compris un 2e bloc) grace a
    raw_decode, qui s'arrete au premier JSON complet.
    """
    text = text.strip()
    fence = _FENCE_RE.search(text)
    if fence:
        text = fence.group(1).strip()
    decoder = json.JSONDecoder()
    # Cherche le 1er debut de structure JSON et decode a partir de la.
    for i, ch in enumerate(text):
        if ch in "[{":
            try:
                obj, _ = decoder.raw_decode(text[i:])
                return obj
            except json.JSONDecodeError:
                continue
    raise ValueError("aucun JSON trouve dans la reponse")
