"""
Practice: answer "why did the assistant say that?" the way Lab 16's script 04 does. An audit log
entry keeps three things: the dated model that answered, the answer itself, and a fingerprint
(hash) of every page the model was shown. You'll compute a page's fingerprint, count what replays
repeat, find the logged answers an edit touched, and name the suspect for one changed answer.

Solution.
"""
import hashlib
import json


def canonical(obj) -> bytes:
    """The same object always gives the same bytes: keys sorted, no spaces, text kept as UTF-8."""
    return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()


def doc_hash(doc: dict) -> str:
    """A page's fingerprint: SHA-256 of canonical({"title", "text", "groups"}), first 16 hex digits.
    The groups are in it too: a page whose permissions changed is a different page."""
    return hashlib.sha256(canonical({"title": doc["title"], "text": doc["text"], "groups": doc["groups"]})).hexdigest()[:16]


def replay_counts(groups: list[dict]) -> dict:
    """Each group is one logged request asked again: {"texts": [logged, r1, ..., r5], "facts": [...]}.
    texts[0] and facts[0] are the logged answer and whether it gave the expected fact.
    Count, over the replays (texts[1:]), compared with the logged answer:
      n          replays in total
      same_text  replays identical to the logged text, word for word
      all_same   requests whose replays are all identical to the logged text
      same_fact  replays that gave the fact, or left it out, the same way as the logged answer"""
    out = {"n": 0, "same_text": 0, "all_same": 0, "same_fact": 0}
    for g in groups:
        first, rest = g["texts"][0], g["texts"][1:]
        out["n"] += len(rest)
        out["same_text"] += sum(t == first for t in rest)
        out["all_same"] += all(t == first for t in rest)
        out["same_fact"] += sum(f == g["facts"][0] for f in g["facts"][1:])
    return out


def shown_an_edited_page(shown: list[list[str]], old: dict, new: dict) -> int:
    """shown: the page ids each logged answer was shown. old / new: page id -> fingerprint, at
    logging time and today. How many logged answers were shown at least one page that changed?"""
    return sum(any(old[i] != new[i] for i in ids) for ids in shown)


def diagnose(entry: dict, reply_model: str, hashes_now: dict) -> dict:
    """entry: a log entry with "model_returned" (the dated version that answered) and "doc_hashes"
    (page id -> fingerprint, as logged). reply_model: the dated version named by a reply today.
    Return {"model_changed": bool, "pages_changed": sorted ids whose fingerprint differs today}."""
    return {
        "model_changed": entry["model_returned"] != reply_model,
        "pages_changed": sorted(i for i, h in entry["doc_hashes"].items() if hashes_now.get(i) != h),
    }


def changed_facts(rows: list[list]) -> dict:
    """rows: [target page id, gave the fact then, gives it now] for each logged answer that was shown
    an edited page and asked again today. Return {target: count} for the answers whose fact changed."""
    out = {}
    for target, then, now in rows:
        if then != now:
            out[target] = out.get(target, 0) + 1
    return out
