"""
Practice: check what an agent did, and let the failure grid pick the fix. Lab 09's checker
(world.check_task), its step labels (first_failure, the heatmap) and its harness retry.

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

MONEY_TOOLS = {"issue_refund", "cancel_order", "update_address"}  # an unexpected one fails a task
STATES = ["start", "identify", "find order", "details", "act", "answer"]


def normalize(text: str) -> str:
    """Lowercase, with curly apostrophes straightened (models write ’)."""
    return text.lower().replace("’", "'").replace("‘", "'")


def arg_matches(key: str, want, got) -> bool:
    """An expected argument matches if it's equal ignoring case and spaces; amounts within a cent."""
    if got is None:
        return False
    if key == "amount":
        try:
            return abs(float(got) - float(want)) <= 0.011
        except (TypeError, ValueError):
            return False
    return str(want).strip().upper() == str(got).strip().upper()


def check_run(task: dict, log: list[dict], answer: str | None) -> list[str]:
    """Judge one run on what changed in the shop, and the facts in its reply. Never on "done".

    task:   {"expect": [{"tool", "args"}], "forbid": [{"tool", "order_id"}], "answer": [[phrase, ...], ...]}
            ("order_id": "any" forbids that tool on every order)
    log:    the writes the shop recorded, [{"tool", "args"}], in order
    answer: the agent's final reply, or None

    Return the reasons it failed (an empty list means it passed):
      - "missing <tool>"     for each expected write with no matching write in the log
                             (same tool, every expected arg matches; each write can match only one)
      - "unexpected <tool>"  for each write left over that is a money tool, or forbidden for its order
      - "no final answer"    if answer is None
      - "answer lacks <first phrase of the group>"  for each group with none of its phrases in the reply

    TODO 1
    """
    return []


def first_failure(labels: list[tuple[str, bool]]) -> tuple[str, str] | None:
    """labels: each step's (state, ok), ending with ("answer", passed).
    Return (the last state that went right, the state of the first wrong step), starting from "start",
    or None if nothing went wrong.

    TODO 2
    """
    return None


def heatmap(pairs: list[tuple[str, str]]) -> list[list[int]]:
    """Count each (last right, first wrong) pair in a 6 x 6 grid, rows and columns in STATES order.

    TODO 3
    """
    return [[0] * 6 for _ in STATES]


def busiest(grid: list[list[int]]) -> tuple[str, str, int]:
    """The cell with the most failed runs, as (row state, column state, count). Ties: the first one
    found reading row by row.

    TODO 4
    """
    return ("start", "start", 0)


OUTAGE = re.compile(r"trouble|unavailable|\b503\b|temporar|try again", re.I)


def told_about_outage(reply: str) -> bool:
    """Did the reply pass the outage on to the customer? Match the outage words above.
    (The lab first matched "issue" too, which also matches "issued a refund".)

    TODO 5
    """
    return False


def call_with_retries(call, retries: int) -> tuple[dict, str, int]:
    """The harness's fix 1. `call()` returns (result, status). While the status is "transient" (a 503),
    try again, up to `retries` more times. Return (result, status, attempts made).

    TODO 6
    """
    result, status = call()
    return result, status, 1
