"""Web Bot Auth (RFC 9421 HTTP Message Signatures, bot-crawler profile).

Reusable signing/verification support for outbound HTTP requests made *by a
P2HS-operated agent acting on P2HS's own behalf* (e.g. a future research or
site-import crawler) — never for a single per-request user-supplied
destination. Do not wire this into `POST .../prompt-media/screenshot` or any
other endpoint that fetches a URL a user pasted in; that fetch is already
SSRF-guarded (src/core/ssrf.py) and identifying it as a "P2HS bot" to the
target site would misrepresent a one-off user action as persistent bot
identity, which is exactly what Web Bot Auth is not for.

As of this writing P2HS has no such outbound crawler, so nothing calls this
module in production and no key directory is published. See the "Web Bot
Auth" row of the discovery deliverable table for the exact activation
prerequisite. This module exists so that whenever such a workflow ships,
turning it on is "generate a key, store it, call sign_request()" rather than
building the RFC 9421 plumbing under deadline.

Reference: https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/
(a bot-flavored profile of RFC 9421, draft-meunier-web-bot-auth-architecture).
"""

from __future__ import annotations

import base64
import hashlib
import time
from dataclasses import dataclass

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption

KEY_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory"
DIRECTORY_MEDIA_TYPE = "application/http-message-signatures-directory+json"
_SIG_LABEL = "sig1"


def _b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()


@dataclass(frozen=True)
class SigningKey:
    """An Ed25519 keypair for Web Bot Auth. Store `private_bytes` in a secret
    manager (never in the database or logs) and rotate by minting a new
    SigningKey while keeping the previous `keyid` published for its overlap
    window (RFC 9421 keyid lets verifiers pick the right public key)."""

    keyid: str
    private_key: Ed25519PrivateKey

    @property
    def public_key(self) -> Ed25519PublicKey:
        return self.private_key.public_key()

    @property
    def public_jwk_thumbprint_keyid(self) -> str:
        """RFC 7638 JWK thumbprint, the conventional `keyid` value."""
        x = _b64url(self.public_key.public_bytes(Encoding.Raw, PublicFormat.Raw))
        canonical = f'{{"crv":"Ed25519","kty":"OKP","x":"{x}"}}'.encode()
        return _b64url(hashlib.sha256(canonical).digest())

    def to_jwk(self) -> dict:
        x = _b64url(self.public_key.public_bytes(Encoding.Raw, PublicFormat.Raw))
        return {"kty": "OKP", "crv": "Ed25519", "x": x, "kid": self.keyid, "use": "sig", "alg": "Ed25519"}

    def private_bytes_pkcs8(self) -> bytes:
        return self.private_key.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())


def generate_signing_key() -> SigningKey:
    """Generate a new Ed25519 keypair. `keyid` defaults to its JWK thumbprint
    so rotation never needs a separate id-allocation step."""
    private_key = Ed25519PrivateKey.generate()
    key = SigningKey(keyid="", private_key=private_key)
    return SigningKey(keyid=key.public_jwk_thumbprint_keyid, private_key=private_key)


def load_signing_key(keyid: str, private_bytes_pkcs8: bytes) -> SigningKey:
    from cryptography.hazmat.primitives.serialization import load_der_private_key
    private_key = load_der_private_key(private_bytes_pkcs8, password=None)
    if not isinstance(private_key, Ed25519PrivateKey):
        raise ValueError("Web Bot Auth requires an Ed25519 private key")
    return SigningKey(keyid=keyid, private_key=private_key)


def _signature_base(*, authority: str, signature_agent: str, params_line: str) -> bytes:
    lines = [
        f'"@authority": {authority}',
        f'"signature-agent": {signature_agent}',
        f'"@signature-params": {params_line}',
    ]
    return "\n".join(lines).encode()


def sign_request_headers(
    *,
    authority: str,
    signing_key: SigningKey,
    signature_agent_url: str,
    ttl_seconds: int = 300,
    now: int | None = None,
) -> dict[str, str]:
    """Build the three headers a verified bot attaches to an outbound request:
    Signature-Agent, Signature-Input, and Signature. `authority` is the
    *target* host (e.g. "example.com"), not P2HS's own host."""
    if not signature_agent_url.startswith("https://"):
        raise ValueError("Signature-Agent must be an HTTPS URL")
    created = now if now is not None else int(time.time())
    expires = created + ttl_seconds
    agent_value = f'"{signature_agent_url}"'
    params_line = (
        f'("@authority" "signature-agent");created={created};expires={expires};'
        f'keyid="{signing_key.keyid}";alg="ed25519";tag="web-bot-auth"'
    )
    base = _signature_base(authority=authority, signature_agent=agent_value, params_line=params_line)
    signature = signing_key.private_key.sign(base)
    return {
        "Signature-Agent": agent_value,
        "Signature-Input": f"{_SIG_LABEL}={params_line}",
        "Signature": f"{_SIG_LABEL}=:{base64.b64encode(signature).decode()}:",
    }


def verify_signed_headers(
    *, authority: str, headers: dict[str, str], public_key: Ed25519PublicKey, now: int | None = None
) -> bool:
    """Verify a signature this module produced. Used by the round-trip test
    below and by any future consumer that needs to self-check before sending."""
    signature_input = headers.get("Signature-Input", "")
    signature_header = headers.get("Signature", "")
    signature_agent = headers.get("Signature-Agent", "")
    if not signature_input.startswith(f"{_SIG_LABEL}=") or not signature_header.startswith(f"{_SIG_LABEL}=:"):
        return False
    params_line = signature_input[len(f"{_SIG_LABEL}="):]
    if 'tag="web-bot-auth"' not in params_line or '"signature-agent"' not in params_line.split(";")[0]:
        return False
    current = now if now is not None else int(time.time())
    try:
        expires = int(next(p.split("=", 1)[1] for p in params_line.split(";") if p.startswith("expires=")))
    except StopIteration:
        return False
    if current > expires:
        return False
    encoded_sig = signature_header[len(f"{_SIG_LABEL}=:"):-1] if signature_header.endswith(":") else ""
    try:
        signature = base64.b64decode(encoded_sig)
    except Exception:
        return False
    base = _signature_base(authority=authority, signature_agent=signature_agent, params_line=params_line)
    try:
        public_key.verify(signature, base)
        return True
    except Exception:
        return False


def build_key_directory(*, keys: list[SigningKey], signing_key: SigningKey, signature_agent_url: str) -> tuple[str, dict[str, str]]:
    """Self-signed JWKS per the Web Bot Auth key-directory convention.
    `keys` should include every currently-valid key (old + new during
    rotation); `signing_key` (normally the newest key) signs the directory
    itself so a verifier can bootstrap trust from the directory response."""
    import json

    body = json.dumps({"keys": [key.to_jwk() for key in keys]}, separators=(",", ":"), sort_keys=True)
    authority = signature_agent_url.split("://", 1)[-1].split("/", 1)[0]
    created = int(time.time())
    expires = created + 86400
    params_line = (
        f'("@authority");created={created};expires={expires};'
        f'keyid="{signing_key.keyid}";alg="ed25519";tag="http-message-signatures-directory"'
    )
    base = f'"@authority": {authority}\n"@signature-params": {params_line}'.encode()
    signature = signing_key.private_key.sign(base)
    headers = {
        "Content-Type": DIRECTORY_MEDIA_TYPE,
        "Signature-Input": f"{_SIG_LABEL}={params_line}",
        "Signature": f"{_SIG_LABEL}=:{base64.b64encode(signature).decode()}:",
        "Cache-Control": "public, max-age=86400",
    }
    return body, headers
