"""
Solution: check what an agent did, and let the failure grid pick the fix (Lab 09's world.py,
metrics.py and harness.py).
"""
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:
    return text.lower().replace("’", "'").replace("‘", "'")


def arg_matches(key: str, want, got) -> bool:
    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]:
    reasons = []
    writes = list(log)
    for exp in task["expect"]:
        hit = next((w for w in writes if w["tool"] == exp["tool"]
                    and all(arg_matches(k, v, w["args"].get(k)) for k, v in exp["args"].items())), None)
        if hit:
            writes.remove(hit)
        else:
            reasons.append(f"missing {exp['tool']}")
    for w in writes:
        forbidden = any(f["tool"] == w["tool"] and f["order_id"] in ("any", w["args"].get("order_id"))
                        for f in task["forbid"])
        if w["tool"] in MONEY_TOOLS or forbidden:
            reasons.append(f"unexpected {w['tool']}")
    if answer is None:
        reasons.append("no final answer")
    else:
        text = normalize(answer)
        for group in task["answer"]:
            if not any(normalize(p) in text for p in group):
                reasons.append(f"answer lacks {group[0]}")
    return reasons


def first_failure(labels: list[tuple[str, bool]]) -> tuple[str, str] | None:
    prev = "start"
    for state, ok in labels:
        if not ok:
            return prev, state
        prev = state
    return None


def heatmap(pairs: list[tuple[str, str]]) -> list[list[int]]:
    grid = [[0] * 6 for _ in STATES]
    for a, b in pairs:
        grid[STATES.index(a)][STATES.index(b)] += 1
    return grid


def busiest(grid: list[list[int]]) -> tuple[str, str, int]:
    best = ("start", "start", -1)
    for i, row in enumerate(grid):
        for j, n in enumerate(row):
            if n > best[2]:
                best = (STATES[i], STATES[j], n)
    return best


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


def told_about_outage(reply: str) -> bool:
    return bool(OUTAGE.search(reply or ""))


def call_with_retries(call, retries: int) -> tuple[dict, str, int]:
    for attempt in range(retries + 1):
        result, status = call()
        if status != "transient":
            break
    return result, status, attempt + 1
