"""
Solution: pin the tools an agent is given, and check them on every connect. Lab 15's
script 05 (and its pinning.py): a fingerprint of each approved tool, a check that reports
what changed, look-alike names, and the rule for which tools the model may see.

The lab's pinning.py, trimmed: findings keep only kind, server and tool.
"""
import hashlib
import json

# Characters that look alike, folded together when comparing names (get_0rder -> get_order).
CONFUSABLE = str.maketrans({"0": "o", "1": "l", "i": "l", "5": "s", "3": "e", "-": "_", ".": "_"})


def fingerprint(tool):
    """sha256 (hex) of the tool's definition in a canonical form: json.dumps of
    {"name", "description", "schema"} (description defaults to "", schema to the tool's
    "input_schema" or {}), with sort_keys=True and separators=(",", ":")."""
    blob = json.dumps({"name": tool["name"], "description": tool.get("description") or "",
                       "schema": tool.get("input_schema") or {}}, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(blob.encode()).hexdigest()


def pin(servers):
    """What a person approved: {server: {tool name: {"fingerprint", "description", "schema"}}}."""
    return {s: {t["name"]: {"fingerprint": fingerprint(t), "description": t.get("description") or "",
                            "schema": t.get("input_schema") or {}} for t in tools}
            for s, tools in servers.items()}


def skeleton(name):
    """A name reduced to what it looks like."""
    return name.lower().translate(CONFUSABLE)


def check(pins, servers):
    """Findings since approval: new servers and tools, changed and removed tools, name collisions."""
    out = []
    for s, tools in servers.items():
        approved = pins.get(s)
        if approved is None:
            out.append({"kind": "new_server", "server": s, "tool": None})
            continue
        for t in tools:
            p = approved.get(t["name"])
            if p is None:
                out.append({"kind": "new_tool", "server": s, "tool": t["name"]})
            elif fingerprint(t) != p["fingerprint"]:
                if (t.get("description") or "") != p["description"]:
                    out.append({"kind": "changed_description", "server": s, "tool": t["name"]})
                if (t.get("input_schema") or {}) != p["schema"]:
                    out.append({"kind": "changed_schema", "server": s, "tool": t["name"]})
        names = {t["name"] for t in tools}
        for name in sorted(set(approved) - names):
            out.append({"kind": "removed_tool", "server": s, "tool": name})
    seen = {}
    for s, tools in servers.items():
        for t in tools:
            k = skeleton(t["name"])
            if any(other != s for other in seen.get(k, [])):
                out.append({"kind": "name_collision", "server": s, "tool": t["name"]})
            seen.setdefault(k, []).append(s)
    return out


def allowed(pins, servers):
    """The (server, tool) pairs the model may see: pinned, and named in no finding."""
    findings = check(pins, servers)
    blocked = {(f["server"], f["tool"]) for f in findings}
    dead = {f["server"] for f in findings if f["tool"] is None}
    return {(s, t["name"]) for s, tools in servers.items() if s not in dead and s in pins
            for t in tools if t["name"] in pins[s] and (s, t["name"]) not in blocked}
