from __future__ import annotations
import json, re, uuid
from pathlib import Path

WORD_RE = re.compile(r"[A-Za-zÀ-ÖØ-öø-ÿ0-9_'-]+", re.UNICODE)

def tokens(text):
    return [x.lower() for x in WORD_RE.findall(text or "")]

def clamp(x):
    return max(0.0, min(1.0, x))

class DestinationRegistry:
    def __init__(self, destinations):
        self.destinations = destinations

    @classmethod
    def from_json(cls, path):
        data = json.loads(Path(path).read_text(encoding="utf-8"))
        return cls(data["destinations"])

class SemanticRouter:
    def __init__(self, registry):
        self.registry = registry

    def score(self, request, destination):
        text = set(tokens(request["text"]))
        cap_words, cap_ids = [], []
        for cap in destination.get("capabilities", []):
            cap_ids.append(cap["id"])
            cap_words += tokens(" ".join([
                cap.get("label",""), cap.get("description",""),
                " ".join(cap.get("keywords",[]))
            ]))
        dest_words = tokens(destination.get("label","") + " " + destination.get("description","")) + cap_words
        universe = set(dest_words)
        inter = len(text & universe)
        union = len(text | universe) or 1
        keyword_hits = len(text & set(cap_words))
        semantic = clamp((inter/union)*2.4 + min(keyword_hits*0.08, 0.35))

        required = request.get("required_capabilities", [])
        capability = (len(set(required) & set(cap_ids))/len(required)) if required else clamp(semantic*0.85+0.15)

        admissible, reasons = True, []
        if not destination.get("available", True):
            admissible=False; reasons.append("destination indisponible")
        if request.get("confidentiality","public") not in destination.get("accepts_confidentiality",["public"]):
            admissible=False; reasons.append("confidentialité non acceptée")
        if request.get("require_human_review",False) and not destination.get("human_review",False):
            admissible=False; reasons.append("revue humaine requise")

        total = clamp(0.42*semantic + 0.33*capability + 0.15 + 0.10) if admissible else 0.0
        reasons.append("forte adéquation sémantique" if semantic > .6 else "adéquation partielle" if semantic > .3 else "faible adéquation")
        return {
            "destination_id":destination["id"],"semantic_fit":round(semantic,4),
            "capability_fit":round(capability,4),"context_fit":1.0,
            "operational_fit":1.0 if destination.get("available",True) else 0.0,
            "risk_penalty":0.0,"total":round(total,4),"admissible":admissible,
            "reasons":reasons
        }

    def route(self, request):
        candidates=sorted([self.score(request,d) for d in self.registry.destinations],key=lambda x:x["total"],reverse=True)
        admissible=[c for c in candidates if c["admissible"]]
        action, selected, confidence = "abstain", [], 0.0
        summary="Aucune destination admissible."
        if admissible:
            best=admissible[0]
            second=admissible[1] if len(admissible)>1 else None
            margin=best["total"]-(second["total"] if second else 0.0)
            if best["total"] < .28:
                action="clarify"; summary="Les signaux sont trop faibles."
            elif best["total"] < .45 or margin < .05:
                action="clarify"; selected=[best["destination_id"]]; confidence=best["total"]; summary="La confiance ou la marge est insuffisante."
            else:
                action="route"; selected=[best["destination_id"]]; confidence=best["total"]; summary="Meilleure cohérence admissible."
        return {
            "route_id":"route_"+uuid.uuid4().hex[:12],"request_id":request["id"],
            "action":action,"selected":selected,"confidence":round(confidence,4),
            "candidates":candidates,
            "explanation":{"summary":summary,"method":"transparent_weighted_scoring_v1",
              "weights":{"semantic_fit":.42,"capability_fit":.33,"context_fit":.15,"operational_fit":.10}},
            "trace_id":"trace_"+uuid.uuid4().hex[:12]
        }
