"""
Practice: the request pipeline of a multi-tenant MCP server, ported to Python from the
acme-crm-mcp demo (src/pipeline.ts, src/observability.ts).

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

ISSUER = "http://localhost:8788"
AUDIENCE = "http://localhost:8787/mcp"           # this server
TOOL_SCOPES = {"search_contacts": "crm:read", "create_contact": "crm:write", "delete_contact": "crm:write"}
WRITE_TOOLS = {"create_contact", "delete_contact"}


def authenticate(signature_ok: bool, claims: dict | None, now: float) -> tuple[int, dict | None]:
    """Step 1. `claims` is the token's payload (None: no bearer token at all); `signature_ok` says
    whether it was signed with the authorization server's key.

    Return (401, None) if there's no token, the signature is bad, iss != ISSUER, aud != AUDIENCE,
    exp <= now, or org_id is missing. Otherwise (200, auth) with
        auth = {"tenant": claims["org_id"], "client": claims["client_id"],
                "scopes": claims["scope"].split()}
    The tenant comes from the validated token, and only from there.

    TODO 1
    """
    return 200, None


class RateLimiter:
    """Step 2: a token bucket per tenant. Each bucket starts full (`burst` tokens) and refills at
    per_minute / 60 tokens per second, up to `burst`.

    allow(tenant, now) refills the tenant's bucket for the time since its last call, then:
      - if it has at least 1 token: take one, return (True, 0)
      - otherwise return (False, retry_after) with retry_after = ceil((1 - tokens) / refill_per_second)

    TODO 2
    """

    def __init__(self, per_minute: int = 120, burst: int = 20):
        self.per_minute, self.burst = per_minute, burst

    def allow(self, tenant: str, now: float) -> tuple[bool, int]:
        return True, 0


def handle(signature_ok: bool, claims: dict | None, tool: str, limiter: RateLimiter, now: float,
           audit: list[dict]) -> int:
    """The pipeline, in order: authenticate → rate limit → scope. Return the HTTP status:
      401 from authenticate; 429 if the tenant's bucket is empty; 403 if the tool needs a scope
      the token doesn't have; otherwise 200 (the tool runs; an unknown tool is answered by the MCP
      server itself with a tool error, so there is no scope to check).
    A denied WRITE tool appends {"tenant", "client", "tool", "outcome": "denied"} to `audit`.
    An unauthenticated call must not touch any tenant's bucket.

    TODO 3
    """
    return 200


def error_rate(responses: list[tuple[int, bool]]) -> float:
    """The monitor's error rate over a window. Each response is (http_status, result_is_error).
    A failure is any status >= 400, OR a 200 whose result has isError: tool errors (bad arguments,
    unknown tools, a failing database) come back as HTTP 200. Return failed / all (0.0 if empty).

    TODO 4
    """
    return 0.0
