"""
Solution: rules in code that take away one leg of the lethal trifecta. Lab 15's egress.py:
the agent may read private data and text anyone can write, but it may only send to people
the *user* named, and may only delete or cancel when the user asked to.

No model, no score: the same answer every time, whatever the planted note says.
"""
import re

EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")

# Tool -> the arguments that hold recipients.
OUTBOUND = {
    "send_email": ("recipients", "cc", "bcc"),
    "share_file": ("email",),
    "create_calendar_event": ("participants",),
    "add_calendar_event_participants": ("participants",),
}
# Tool -> the verb the user must have used.
DESTRUCTIVE = {"delete_email": "delete", "delete_file": "delete", "cancel_calendar_event": "cancel"}


def addresses(value) -> set:
    """Every email address in an argument, lowercased, whether it's a list, a string, a list
    written as a string, or None."""
    return {a.lower() for a in EMAIL.findall(str(value or ""))}


def check(tool: str, args: dict, user_prompt: str, allow_domains: tuple = ()):
    """None if the call may run, otherwise the reason it's blocked."""
    if tool in OUTBOUND:
        trusted = addresses(user_prompt)
        for field in OUTBOUND[tool]:
            for a in sorted(addresses(args.get(field))):
                if a not in trusted and not any(a.endswith("@" + d) for d in allow_domains):
                    return f"{tool} to {a}: not an address from the user's request"
    if tool in DESTRUCTIVE and DESTRUCTIVE[tool] not in user_prompt.lower():
        return f"{tool}: the user didn't ask to {DESTRUCTIVE[tool]} anything"
    return None


def replay(calls, user_prompt: str, allow_domains: tuple = ()):
    """Run the rules over an agent's tool calls, each a (tool, args) pair, in order.
    Returns the blocked ones as (tool, args, reason)."""
    blocked = []
    for tool, args in calls:
        reason = check(tool, args, user_prompt, allow_domains)
        if reason:
            blocked.append((tool, args, reason))
    return blocked
