"""Chore Network door — Python port. Needs only `cryptography` (pip install cryptography).

    from door import chore_door
    verdict = chore_door(method, host, target, body, headers, site="site:…", network_key="<x from https://chore.network/.well-known/chore.json>")
    if verdict is None: serve the page            # human, or agent with a valid ticket
    else: status, body_json = verdict             # (401, {...}) or (402, {...})

Flask example:
    @app.before_request
    def door():
        v = chore_door(request.method, request.host, request.full_path.rstrip("?"), request.get_data(), dict(request.headers), site=SITE, network_key=NETWORK_KEY)
        if v: return jsonify(v[1]), v[0]
Humans (no X-Chore-Key) always pass. Nothing here calls the network.
"""
from __future__ import annotations

import base64
import hashlib
import json
import time

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

NETWORK = "https://chore.network"


def _unb64u(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def verify_request(method: str, host: str, target: str, body: bytes, headers: dict, *, window_ms: int = 300_000, now_ms: int | None = None):
    """Returns the agent id (str) if the chore-v1 signature is valid, else (None, reason)."""
    h = {k.lower(): v for k, v in headers.items()}
    key, ts, nonce, sig = h.get("x-chore-key"), h.get("x-chore-ts"), h.get("x-chore-nonce"), h.get("x-chore-sig")
    if not (key and ts and nonce and sig):
        return None, "missing_signature_headers"
    try:
        pub, sg = _unb64u(key), _unb64u(sig)
    except Exception:
        return None, "bad_key_or_sig_encoding"
    if len(pub) != 32 or len(sg) != 64:
        return None, "bad_key_or_sig_encoding"
    now = now_ms if now_ms is not None else int(time.time() * 1000)
    try:
        if abs(now - int(ts)) > window_ms:
            return None, "timestamp_out_of_window"
    except ValueError:
        return None, "timestamp_out_of_window"
    if host.endswith(":443") or host.endswith(":80"):
        host = host.rsplit(":", 1)[0]
    msg = "\n".join(["chore-v1", method.upper(), host.lower(), target, ts, nonce, hashlib.sha256(body or b"").hexdigest()]).encode()
    try:
        Ed25519PublicKey.from_public_bytes(pub).verify(sg, msg)
    except Exception:
        return None, "bad_signature"
    return hashlib.sha256(pub).hexdigest()[:32], None


def verify_ticket(token: str | None, agent_id: str, site: str, network_key: str, *, now_ms: int | None = None) -> bool:
    if not token:
        return False
    parts = token.split(".")
    if len(parts) != 3 or parts[0] != "v1":
        return False
    try:
        payload, sig = _unb64u(parts[1]), _unb64u(parts[2])
        Ed25519PublicKey.from_public_bytes(_unb64u(network_key)).verify(sig, payload)
        t = json.loads(payload)
    except Exception:
        return False
    now = now_ms if now_ms is not None else int(time.time() * 1000)
    return t.get("a") == agent_id and t.get("s") == site and t.get("exp", 0) > now


def chore_door(method: str, host: str, target: str, body: bytes, headers: dict, *, site: str, network_key: str,
               network: str = NETWORK, now_ms: int | None = None):
    """None = let the request through. Otherwise (status, json_body) to send back."""
    h = {k.lower(): v for k, v in headers.items()}
    if "x-chore-key" not in h:
        return None
    agent_id, reason = verify_request(method, host, target, body, headers, now_ms=now_ms)
    if agent_id is None:
        return 401, {"error": "bad_signature", "reason": reason}
    if verify_ticket(h.get("x-chore-ticket"), agent_id, site, network_key, now_ms=now_ms):
        return None
    network = network.rstrip("/")
    return 402, {"error": "ticket_required", "site": site, "network": network,
                 "challenge": f"{network}/chore/challenge?site={site}", "submit": f"{network}/chore/submit", "pay": f"{network}/chore/pay"}


if __name__ == "__main__":  # self-test against the shared vectors
    import pathlib
    v = json.loads((pathlib.Path(__file__).parent / "vectors.json").read_text())
    g, now = v["get"], v["now_ms"]
    cfg = dict(site=v["site"], network_key=v["network_key"], now_ms=now)
    ok = []
    def check(name, c): ok.append(c); print(f"[{'PASS' if c else 'FAIL'}] {name}")
    check("no key → pass", chore_door("GET", g["host"], g["target"], b"", {}, **cfg) is None)
    check("signed, no ticket → 402", chore_door("GET", g["host"], g["target"], b"", g["headers"], **cfg)[0] == 402)
    check("signed + ticket → pass", chore_door("GET", g["host"], g["target"], b"", {**g["headers"], "X-Chore-Ticket": g["ticket"]}, **cfg) is None)
    check("host with :443 → pass", chore_door("GET", g["host"] + ":443", g["target"], b"", {**g["headers"], "X-Chore-Ticket": g["ticket"]}, **cfg) is None)
    check("other key with same URL, this ticket → 402", chore_door("GET", g["host"], g["target"], b"", {**v["other_key_same_url"]["headers"], "X-Chore-Ticket": g["ticket"]}, **cfg)[0] == 402)
    check("tampered nonce → 401", chore_door("GET", g["host"], g["target"], b"", {**g["headers"], "X-Chore-Nonce": g["headers"]["X-Chore-Nonce"] + "x"}, **cfg)[0] == 401)
    check("ticket for another site → 402", chore_door("GET", g["host"], g["target"], b"", {**g["headers"], "X-Chore-Ticket": g["ticket"]}, site="site:other", network_key=v["network_key"], now_ms=now)[0] == 402)
    check("expired by clock → 401", chore_door("GET", g["host"], g["target"], b"", g["headers"], site=v["site"], network_key=v["network_key"], now_ms=now + 10 * 60 * 1000)[0] == 401)
    print(f"DONE {sum(ok)}/{len(ok)}"); raise SystemExit(0 if all(ok) else 1)
