"""
Practice: the tool-use scorer and release gate from the acme-crm-mcp demo
(evals/scoring.ts, evals/runToolEvals.ts), ported to Python.

A golden turn has `expected` steps ({"tool", "args"}) and `forbidden` tools. An expected argument
is either an exact value (compared case-insensitively, after trimming) or {"contains": "..."}
for free wording, like a search query. A call is {"tool", "args"}.

Fill in the four TODOs, then run the tests:
    pytest test_scorer_practice.py        (locally)
or press "Run tests" on the chapter page.
"""

READ_ONLY = {"search_contacts"}


def args_match(expected: dict, call: dict) -> bool:
    """Same tool, and every expected argument matches:
      - {"contains": x}: the call's value, normalized, contains x normalized
      - otherwise: normalized values are equal
    normalize = str(value or "").strip().lower(); a missing argument normalizes to "".

    TODO 1
    """
    return False


def in_order(calls: list[dict], expected: list[dict]) -> bool:
    """The expected steps appear in the calls as an in-order subsequence: each expected step is
    matched (args_match) by a call AFTER the call that matched the previous step. Extra calls in
    between are fine.

    TODO 2
    """
    return False


def redundant(calls: list[dict], expected: list[dict]) -> list[str]:
    """Tools called more often than allowed. Each expected step allows one call of its tool;
    a read-only tool gets one extra (e.g. a second search with other wording). Tools that aren't
    expected at all are tool selection's business, not this check's.
    Return "<tool> called <n>× (allowed <m>)" for each, in order of first call.

    TODO 3
    """
    return []


def score_turn(calls: list[dict], turn: dict) -> dict:
    """All four checks; a turn passes only if all hold. (Given.)"""
    called = {c["tool"] for c in calls}
    missing = sorted({e["tool"] for e in turn["expected"]} - called)
    forbidden = [t for t in turn.get("forbidden", []) if t in called]
    tool_selection = not missing and not forbidden
    arguments = all(any(args_match(e, c) for c in calls) for e in turn["expected"] if e["tool"] in called)
    order = in_order(calls, turn["expected"])
    no_redundancy = not redundant(calls, turn["expected"])
    return {"tool_selection": tool_selection, "arguments": arguments, "order": order,
            "no_redundancy": no_redundancy,
            "pass": tool_selection and arguments and order and no_redundancy}


def gate(pass_rates: dict[str, float], threshold: float = 0.95) -> tuple[str, float]:
    """The release gate looks at the WEAKEST model, not the average.
    Return ("PASS" or "BLOCK", weakest rate). An empty dict blocks, with rate 0.0.

    TODO 4
    """
    return "PASS", 1.0
