"""
A governed agent with NO framework — the whole protocol lifecycle in one file.

Every other example here wraps a framework's tool. This one shows what is underneath, in plain Python, because
the parts that are easy to get wrong are not in the wrapper: they are in what you do with each answer.

    authorize ──▶ allow      run the tool; if it calls a gateway, hand it the signed request
             ├─▶ escalate    a HOLD, not a denial: wait for a human; act ONLY on `may_proceed`
             └─▶ block       stop. The tool is never touched.

    then, for a permit:  capture  (the full amount, when YOU executed it)
                         void     (an unclaimed hold you decided not to use)
                         outcome  (did it happen? is a retry safe?)

Run it against a sandbox agent (a pair you can have in about four seconds, no account):

    npx create-metamynd-agent --sandbox

    pip install metamynd-client       # the client (it pulls in cryptography); there is no framework here
    export METAMYND_API=https://metamynd.ai/api/v1
    export AGENT_DID=did:hedera:testnet:...
    export AGENT_KEY=302e020100...            # as issued
    python plain_python_agent.py

To see the gateway handoff for real, point it at a service protected by `@metamynd/agentsafe-http-gateway`:

    export GATEWAY_URL=http://localhost:4000/book      # a route that expects `amount` and `merchant`
"""

from __future__ import annotations

import json
import os
import urllib.request
from typing import Any, Optional

from metamynd_client import GovernanceBlocked, MetaMyndClient, governance_headers, guard_tool

# Built only when the credentials are present, so importing this file never fails: `main()` says what is
# missing instead of dying at import with a stack trace.
client = MetaMyndClient.from_env() if os.getenv("AGENT_DID") and os.getenv("AGENT_KEY") else None  # type: ignore[assignment]

# `riskLevel` is REQUIRED by the starter rules: a call that carries none is escalated, not allowed (a call that
# hides its risk is indistinguishable from one lying about it). 'low' is a PLACEHOLDER, not an assessment — a real
# integration states an honest one, or lets the mandate's owner set a `riskTier` the agent cannot lower.
CONTEXT = {"tool": "book-flight", "riskLevel": "low"}

# ---------------------------------------------------------------------------------------
# 1. Your tool. If it calls a service that re-verifies the agent, it presents the signed request.
# ---------------------------------------------------------------------------------------


def book_flight(airline: str, amount: float, headers: Optional[dict] = None) -> dict[str, Any]:
    """Book a flight. Calls a MAGP-protected service when GATEWAY_URL is set, else books locally.

    `headers` is the signed request when the caller has one in hand (`verdict.signed.headers()`); a call made
    through `guard_tool` needs none, because `governance_headers()` is the signed request of THAT guarded call.
    """
    gateway = os.getenv("GATEWAY_URL")
    if not gateway:
        return {"pnr": "QK7T2M", "airline": airline, "amount": amount, "via": "local"}
    request = urllib.request.Request(
        gateway,
        data=json.dumps({"amount": amount, "merchant": airline}).encode(),
        method="POST",
        # The signed request (`x-magp-request`): the service re-verifies it against the agent's own policy and
        # claims the hold. Without it the service refuses (MISSING_GOVERNANCE), which is the safe failure.
        # Outside a guarded call `governance_headers()` is empty, so the caller must pass it explicitly.
        headers={"Content-Type": "application/json", **(headers if headers is not None else governance_headers())},
    )
    with urllib.request.urlopen(request, timeout=15) as response:
        return {**json.loads(response.read()), "via": "gateway"}


governed_book_flight = guard_tool(
    client,
    "flight-purchase",  # = your mandate scope
    book_flight,
    lambda airline, amount: {"amount": amount, "merchant": airline, "context": CONTEXT},
)

# ---------------------------------------------------------------------------------------
# 2. What to do with each answer
# ---------------------------------------------------------------------------------------


def show(label: str, text: str) -> None:
    print(f"  {label}\n     -> {text}\n")


def main() -> None:
    if client is None:
        raise SystemExit("Set AGENT_DID and AGENT_KEY — run `npx create-metamynd-agent --sandbox` for a free pair.")

    print("\n  A governed agent, no framework\n")

    # ---- allow: the tool runs, and (if it needs one) it can present the signed request ---------
    verdict = client.authorize("flight-purchase", 100, merchant="skyward-air", context=CONTEXT)
    assert verdict.permitted and verdict.signed is not None
    fields = sorted(verdict.signed.body)
    show("1. $100 on skyward-air, authorized", f"{verdict.decision}/{verdict.reason_code}; a gateway would receive the signed request ({', '.join(fields)}) as `x-magp-request`")

    # Executed outside `guard_tool`, so the signed request is passed explicitly. When a gateway executes the
    # action it CLAIMS the hold and settles it itself — the agent must not capture on top. Only when the agent
    # executed the action ITSELF does it settle, at the FULL amount: only a service that executed it may settle
    # LOWER or release, because otherwise an agent could wait for a purchase to happen and then take its budget
    # back.
    booked = book_flight("skyward-air", 100, headers=verdict.signed.headers())
    if booked["via"] == "gateway":
        show("   ...executed by the gateway, which settles what it claimed", f"{booked.get('pnr', 'booked')} via gateway; no capture from the agent")
    else:
        settled = client.capture(verdict.authorization_id, 100, booking_ref=booked["pnr"])
        show("   ...executed, and captured at the full amount", f"{booked['pnr']} via {booked['via']}; capture ok={settled.ok}")
    outcome = client.outcome(verdict.authorization_id)
    show("   ...and what became of it", f"{outcome.outcome} (retry_safe={outcome.retry_safe}) — do NOT authorize it again unless retry_safe")

    # ---- a permit you decide NOT to use: release it, and only THEN is a retry safe -------------
    spare = client.authorize("flight-purchase", 120, merchant="northwind-rail", context=CONTEXT)
    if spare.permitted:
        before = client.outcome(spare.authorization_id)
        # nothing has run, but the hold is still CLAIMABLE until its window closes — so it is not yet retry-safe
        show("2. a second permit, unused", f"{before.outcome}: nothing_executed={before.nothing_executed}, retry_safe={before.retry_safe}")
        client.void(spare.authorization_id, reason="not needed")
        after = client.outcome(spare.authorization_id)
        show("   ...voided", f"{after.outcome}: retry_safe={after.retry_safe} — now a fresh attempt cannot duplicate it")

    # ---- block: stop -----------------------------------------------------------------------------
    try:
        governed_book_flight(airline="skyward-air", amount=2_400)
    except GovernanceBlocked as refused:
        show("3. $2,400 — over the per-transaction cap", f"{refused.verdict.decision}/{refused.verdict.reason_code} — book_flight was never called")

    # ---- escalate: a HOLD. Wait, and act only on `may_proceed` -----------------------------------
    held = client.authorize("flight-purchase", 200, merchant="skyward-air", context={**CONTEXT, "riskLevel": "high"})
    if held.escalation_id:
        status = client.wait_for_escalation(held.escalation_id, timeout=1)  # a real agent waits minutes, not one second
        show("4. high risk — held for a human", f"{held.decision}/{held.reason_code}; after waiting: {status.status} (may_proceed={status.may_proceed})")
        # A hold nobody has decided is a hold: never treat `pending` as a yes, and never approve it yourself.
        # When a human DOES approve, `status.authorization_id` is the hold to act on. The request you signed
        # first is old by then, and a service refuses a stale one — so sign a FRESH one carrying that id:
        #
        #     signed = client.sign_request("flight-purchase", 200, merchant="skyward-air",
        #                                  context=CONTEXT, authorization_id=status.authorization_id)
        #     requests.post(url, headers=signed.headers(), ...)
        if status.may_proceed:
            fresh: Optional[Any] = client.sign_request("flight-purchase", 200, merchant="skyward-air", context=CONTEXT, authorization_id=status.authorization_id)
            show("   ...approved", f"a fresh signed request for {fresh.authorization_id}")

    # ---- the case a prompt could not stop ---------------------------------------------------------
    try:
        guard_tool(client, "permissions.update", lambda **_: {"updated": True})(limit=100_000)
    except GovernanceBlocked as refused:
        show("5. the agent asks to raise its own limit", f"{refused.verdict.decision}/{refused.verdict.reason_code} — the mandate never granted that action")


if __name__ == "__main__":
    main()
