#!/usr/bin/env python3
"""
skill_abi — the FROZEN skill contract (S041 domain-pack ABI). The universal core is domain-blind;
EVERY domain specific lives in a skill that honors this ABI. This module is the single source of
truth for what a skill must supply and how a skill is hardened, conformance-checked, registered,
routed (cheap metadata only), and run. It contains NO domain logic and only stdlib.

A SKILL supplies:
  - MANIFEST : SkillManifest(name, description, when_to_use, version)   # F001 cheap-metadata routing
  - tools    : { tool_name: {"fn": callable, "oracle": callable, "cases": [(input, expected), ...]} }
        fn(input: dict)    -> {"value": ...} | {"refuse": "..."}   right-or-refuse (hardened here)
        oracle(input: dict)-> value                                 F003: independent per-domain oracle (MANDATORY)
        cases              -> >=1 worked fixture/anchor per tool
  - judge / judgment_refusals (optional)   the professional-judgment refusal boundary

The contract this freezes (mirrored by the Zig port):
  P1  every tool is right-or-refuse: fn returns {"value":..} or {"refuse":..}, NEVER raises, NEVER
      a confident wrong answer at its edge (non-dict / garbage input -> typed refusal).
  P2  every tool carries its OWN independent oracle (F003) and >=1 external worked anchor.
  P3  fn is deterministic: fn(x) == fn(x).
  P4  a non-conformant skill FAILS LOUDLY at registration (no silent half-skill).
  P5  routing uses MANIFEST metadata only; it never executes a tool or peeks at an oracle.
"""
from __future__ import annotations
from dataclasses import dataclass
import re


# ── the manifest: cheap metadata a router reads without touching the engine (F001) ──
@dataclass(frozen=True)
class SkillManifest:
    name: str
    description: str          # one line: what the skill computes
    when_to_use: str          # one line: the trigger surface a router matches a query against
    version: str = "1.0.0"

    def __post_init__(self):
        for f in ("name", "description", "when_to_use"):
            v = getattr(self, f)
            if not isinstance(v, str) or not v.strip():
                raise ConformanceError(f"manifest.{f} must be a non-empty string")


class ConformanceError(Exception):
    """A skill that does not honor the ABI. Raised loudly at construction/registration (P4)."""


# ── the boundary fail-safe: the ONE place a tool is hardened to right-or-refuse (P1) ──
def harden(fn):
    """Wrap a raw tool fn so its EDGE is right-or-refuse: a non-record/garbage input or an
    unexpected exception becomes a TYPED REFUSAL, never a crash and never a confident wrong value.

    The WHOLE post-call processing — calling fn AND inspecting its result — runs inside one
    BaseException guard. Not just fn(inp): a tool can return an object whose `__class__` / `__eq__`
    raises, so even `isinstance(out, dict)` / `"value" in out` can raise, and those must refuse too.
    BaseException (not Exception) so a non-Exception raise can't escape either. (audit KILL 2026-06-29)"""
    def g(inp):
        try:
            if not isinstance(inp, dict):             # the INPUT can also be hostile (raising __class__)
                return {"refuse": "input must be a record (a dict of fields)"}
            out = fn(inp)
            if isinstance(out, dict) and ("value" in out or "refuse" in out):
                return out
            return {"refuse": "tool produced no well-formed result"}
        except BaseException as e:                    # boundary fail-safe (AGENT.md §7: named, not swallowed)
            try:
                name = type(e).__name__               # even this could be a hostile descriptor
            except BaseException:
                name = "error"
            return {"refuse": f"could not process input ({name})"}
    return g


# ── conformance: prove a skill honors P1–P3 before it is allowed to register (P4) ──
_GARBAGE = [{}, None, [1, 2, 3], 42, "garbage", {"unexpected": "field"}]


def _norm(x):
    """Normalize a result/expected to a comparable value: {value:..}->value, {refuse:..}->REFUSE."""
    if isinstance(x, dict):
        if "value" in x:
            return x["value"]
        if "refuse" in x:
            return _REFUSE
    return x


_REFUSE = object()   # sentinel: a refusal, distinct from any real value


def check_conformance(manifest, tools, *, sample_cases=True):
    """Return (ok, violations). Checks P1 (hardened right-or-refuse on a garbage battery, never
    raises), P2 (oracle + >=1 worked case per tool, AND the tool AGREES with its own case and
    oracle — a tool that silently refuses or misanswers its own anchor is non-conformant), P3
    (determinism on the tool's own cases). Domain-blind."""
    v = []
    if not isinstance(manifest, SkillManifest):
        v.append("manifest is not a SkillManifest")
    if not tools:
        v.append("skill exposes no tools")
    for name, t in (tools or {}).items():
        fn, oracle, cases = t.get("fn"), t.get("oracle"), t.get("cases")
        if not callable(fn):
            v.append(f"{name}: fn not callable"); continue
        if not callable(oracle):                                   # F003: per-domain oracle MANDATORY
            v.append(f"{name}: missing independent oracle (F003)")
        if not cases:
            v.append(f"{name}: no worked anchor/case (P2)")
        # P1: the hardened fn must never raise and must always return a well-formed result
        hfn = harden(fn)
        for g in _GARBAGE:
            try:
                out = hfn(g)
            except Exception as e:
                v.append(f"{name}: hardened fn raised on garbage {g!r} ({type(e).__name__})"); continue
            if not (isinstance(out, dict) and ("value" in out or "refuse" in out)):
                v.append(f"{name}: garbage {g!r} gave non-result {out!r}")
        # P2 (correctness) + P3 (determinism) on the tool's OWN declared cases
        if sample_cases and cases:
            for inp, expected in cases:
                try:
                    out1, out2 = hfn(inp), hfn(inp)
                except Exception as e:
                    v.append(f"{name}: case raised {type(e).__name__} on {str(inp)[:40]}"); continue
                if out1 != out2:
                    v.append(f"{name}: non-deterministic on case {str(inp)[:40]}")
                exp = _norm(expected)
                got = _norm(out1)
                if exp is _REFUSE:
                    if got is not _REFUSE:
                        v.append(f"{name}: case expects refusal but tool answered {str(inp)[:40]}")
                else:
                    if got is _REFUSE:
                        v.append(f"{name}: tool refused its own valid case {str(inp)[:40]}")
                    elif got != exp:
                        v.append(f"{name}: case mismatch on {str(inp)[:40]} (got {got!r} exp {exp!r})")
                    elif callable(oracle):
                        try:
                            if _norm(oracle(inp)) != got:
                                v.append(f"{name}: disagrees with its own oracle on {str(inp)[:40]}")
                        except Exception as e:
                            v.append(f"{name}: oracle raised {type(e).__name__} on {str(inp)[:40]}")
    return (len(v) == 0, v)


# ── the Skill: a conformant, hardened, runnable unit (the engine sees skills only through this) ──
class Skill:
    """Assembles raw domain tools into a hardened, conformance-checked skill. Construction FAILS
    LOUDLY (ConformanceError) if the ABI is not honored (P4). `tools` carry hardened fns; `oracle`
    is kept raw for an independent gate's cross-check."""
    def __init__(self, manifest, tools, *, judge=None, judgment_refusals=None):
        ok, violations = check_conformance(manifest, tools)
        if not ok:
            raise ConformanceError(f"skill {getattr(manifest,'name','?')!r} non-conformant: {violations}")
        self.manifest = manifest
        self.tools = {n: {**t, "fn": harden(t["fn"])} for n, t in tools.items()}  # hardened public fns
        self._raw = tools                                                          # raw oracle/cases kept
        self._judge = judge
        self.judgment_refusals = tuple(judgment_refusals or ())

    @property
    def name(self):
        return self.manifest.name

    def tool_names(self):
        return tuple(self.tools.keys())

    def oracle(self, tool_name):
        """The raw independent oracle for a tool (gate-only; F003 cross-check)."""
        return self._raw[tool_name]["oracle"]

    def cases(self, tool_name):
        return self._raw[tool_name]["cases"]

    def run(self, tool_name, inp):
        """Single entry: run a named tool right-or-refuse, or refuse an unknown tool."""
        if tool_name not in self.tools:
            return {"refuse": f"unknown tool: {tool_name!r}"}
        return self.tools[tool_name]["fn"](inp)

    def judge(self, question):
        """Route a professional-judgment question to the skill's refusal boundary, if it has one."""
        if self._judge is None:
            return {"refuse": "this skill renders no professional judgment"}
        return self._judge(question)


# ── the registry: reference skills by manifest; route on metadata ONLY (P5) ──
# Stopwords are dropped so routing scores CONTENT overlap, not function-word coincidence
# (without this, an unrelated query matches a manifest on a shared function word like "of").
# General list, no domain terms — a skill's content words carry the signal.
_STOP = frozenset((
    "a an and are as at be by for from in into is it of on or that the to with what which "
    "this these those do does did how when where who whom why will would can could should "
    "i you he she we they not no yes").split())


def _tokens(s):
    return {t for t in re.findall(r"[a-z0-9]+", s.lower()) if t not in _STOP}


class SkillRegistry:
    """Holds conformant skills, addressable by name. `route` matches a query against MANIFEST
    metadata only (cheap, deterministic, no model, no tool execution) — F001 progressive disclosure."""
    def __init__(self):
        self._skills = {}

    def register(self, skill):
        if not isinstance(skill, Skill):
            raise ConformanceError("register expects a Skill (build it via the ABI, which conformance-checks)")
        self._skills[skill.name] = skill
        return skill

    def get(self, name):
        return self._skills.get(name)

    def names(self):
        return tuple(self._skills.keys())

    def manifests(self):
        """The cheap-metadata view a router sees first (no tools, no oracles)."""
        return [s.manifest for s in self._skills.values()]

    def route(self, query):
        """Deterministic keyword-overlap match of `query` against each skill's when_to_use +
        description. Returns [(skill_name, score)] ranked desc, zero-overlap dropped. Metadata
        only — never runs a tool (P5). This is a lexical router, NOT semantic; it ranks candidates,
        the per-domain oracle still verifies-or-refuses the actual answer."""
        q = _tokens(query if isinstance(query, str) else "")
        scored = []
        for s in self._skills.values():
            meta = _tokens(s.manifest.when_to_use + " " + s.manifest.description)
            score = len(q & meta)
            if score:
                scored.append((s.name, score))
        scored.sort(key=lambda x: (-x[1], x[0]))
        return scored

    def run(self, skill_name, tool_name, inp):
        s = self._skills.get(skill_name)
        if s is None:
            return {"refuse": f"unknown skill: {skill_name!r}"}
        return s.run(tool_name, inp)


__all__ = ["SkillManifest", "Skill", "SkillRegistry", "ConformanceError",
           "harden", "check_conformance"]
