"""Cœur anti-invention (#6) : résolution des preuves depuis les hand-offs amont.

Un scénario démo ne contient AUCUN chiffre en dur. Chaque « evidence » du spec est
un pointeur `(module, file, pointer)` vers un artefact `out/` déjà produit par un
module livré. Ce fichier :

  1. charge l'artefact JSON (avec cache) et signale toute absence / JSON invalide ;
  2. résout un pointeur RFC 6901 (`/0/counts/fields`) vers sa valeur verbatim ;
  3. quand la valeur n'existe pas, renvoie un PLACEHOLDER `{{module:file#pointer}}`
     — jamais un chiffre fabriqué, jamais un `0` posé.

La résolution est PURE et rejouable : l'invariant `INV_ANTI_INVENTION` du CLI
re-résout indépendamment chaque pointeur et compare à ce qui figure dans le
run-sheet — impossible de « poser » une valeur à la main.
"""

from __future__ import annotations

import json
import os
from typing import Any

from .deps import DELIVERABLES_ROOT

# Sentinelle « non résolu » — un placeholder lisible, jamais confondable avec une
# vraie valeur (ni un nombre, ni une chaîne métier).
_MISSING = object()


def placeholder(module: str, file: str, pointer: str) -> str:
    return f"{{{{{module}:{file}#{pointer}}}}}"


def is_placeholder(value: Any) -> bool:
    return isinstance(value, str) and value.startswith("{{") and value.endswith("}}")


def artifact_path(module: str, file: str) -> str:
    """Chemin absolu de l'artefact `out/` (jamais hors des livrables)."""
    return os.path.join(DELIVERABLES_ROOT, module, file)


class Loader:
    """Charge et met en cache les artefacts amont pour une passe de build."""

    def __init__(self) -> None:
        self._cache: dict[str, Any] = {}
        self._errors: dict[str, str] = {}

    def load(self, module: str, file: str) -> Any:
        key = f"{module}/{file}"
        if key in self._cache or key in self._errors:
            return self._cache.get(key, _MISSING)
        path = artifact_path(module, file)
        if not os.path.isfile(path):
            self._errors[key] = "artefact absent"
            return _MISSING
        try:
            with open(path, encoding="utf-8") as fh:
                obj = json.load(fh)
        except (OSError, ValueError) as exc:
            self._errors[key] = f"JSON invalide : {exc}"
            return _MISSING
        self._cache[key] = obj
        return obj

    def errors(self) -> dict[str, str]:
        return dict(self._errors)


def _split_pointer(pointer: str) -> list[str]:
    """Découpe un pointeur RFC 6901 (`/a/0/b`) en tokens déséchappés."""
    if pointer == "":
        return []
    if not pointer.startswith("/"):
        raise ValueError(f"pointeur RFC 6901 invalide (doit commencer par '/') : {pointer!r}")
    tokens = pointer.split("/")[1:]
    return [t.replace("~1", "/").replace("~0", "~") for t in tokens]


def _descend(obj: Any, token: str) -> Any:
    """Un pas de descente : clé d'objet ou index de liste (jamais d'erreur)."""
    if isinstance(obj, dict):
        return obj.get(token, _MISSING)
    if isinstance(obj, list):
        if token.lstrip("-").isdigit():
            idx = int(token)
            if -len(obj) <= idx < len(obj):
                return obj[idx]
        return _MISSING
    return _MISSING


def resolve(loader: Loader, module: str, file: str, pointer: str) -> tuple[Any, bool]:
    """Résout un pointeur en `(valeur, resolu)`.

    `resolu=False` ⇒ `valeur` est un placeholder `{{…}}` (artefact absent / JSON
    invalide / chemin inexistant). `resolu=True` ⇒ valeur verbatim non `None`.
    Une valeur littérale `null` dans l'artefact est traitée comme NON résolue :
    un chiffre « à confirmer » ne doit jamais alimenter un pitch (#6).
    """
    root = loader.load(module, file)
    if root is _MISSING:
        return placeholder(module, file, pointer), False
    node: Any = root
    for token in _split_pointer(pointer):
        node = _descend(node, token)
        if node is _MISSING:
            return placeholder(module, file, pointer), False
    if node is None:
        return placeholder(module, file, pointer), False
    return node, True
