"""
CertiSigma SDK — Sync and Async HTTP clients.

Usage:
    from certisigma import CertiSigmaClient

    # Authenticated client (attest + verify)
    client = CertiSigmaClient(api_key="cs_live_xxx")
    result = client.attest("abcdef0123456789..." * 4)

    # Public client (verify only — no API key needed)
    public = CertiSigmaClient()
    check = public.verify("abcdef0123456789..." * 4)
"""

from __future__ import annotations

import hashlib
import re
from typing import Optional

import httpx

from .exceptions import (
    CertiSigmaError,
    AuthenticationError,
    RateLimitError,
    QuotaExceededError,
    NotFoundError,
    ValidationError,
)
from .types import (
    AttestResult,
    VerifyResult,
    BatchAttestResult,
    BatchVerifyResult,
    AttestationStatus,
    MetadataPatchResult,
    EvidenceResult,
    MetadataResult,
    ShareTokenResult,
    ShareTokenInfo,
    Tag,
    TagResult,
    TagQueryResult,
    DerivedListResult,
    DerivedListInfo,
    DerivedListDetail,
    MatchResult,
    DerivedListSignature,
    AccessLogEntry,
)

DEFAULT_BASE_URL = "https://api.certisigma.ch"
SDK_USER_AGENT = "certisigma-python/1.7.0"
SHA256_RE = re.compile(r"^[a-f0-9]{64}$")


def _validate_hash(h: str) -> str:
    h = h.lower().strip()
    if not SHA256_RE.match(h):
        raise ValueError(f"Invalid SHA-256 hash: must be 64 hex chars, got {len(h)}")
    return h


class _BaseClient:
    """Shared logic for sync/async clients."""

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = 30.0,
    ):
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._headers: dict[str, str] = {
            "Content-Type": "application/json",
            "User-Agent": SDK_USER_AGENT,
        }
        if api_key:
            self._headers["Authorization"] = f"Bearer {api_key}"

    def _require_auth(self) -> None:
        if not self._api_key:
            raise AuthenticationError(
                "api_key is required for this operation. "
                "Pass api_key to CertiSigmaClient() or use a public endpoint (verify, batch_verify, health).",
                status_code=None, response=None,
            )

    def _handle_error(self, resp: httpx.Response) -> None:
        if resp.status_code < 400:
            return
        try:
            body = resp.json()
        except Exception:
            body = {"detail": resp.text}

        msg = body.get("detail", body.get("message", str(resp.status_code)))
        kwargs = {"status_code": resp.status_code, "response": body}

        if resp.status_code == 401:
            raise AuthenticationError(msg, **kwargs)
        if resp.status_code == 429:
            if "quota" in msg.lower():
                raise QuotaExceededError(msg, **kwargs)
            retry = int(resp.headers.get("Retry-After", "60"))
            raise RateLimitError(msg, retry_after=retry, **kwargs)
        if resp.status_code == 404:
            raise NotFoundError(msg, **kwargs)
        if resp.status_code in (400, 422):
            raise ValidationError(msg, **kwargs)
        raise CertiSigmaError(msg, **kwargs)


class CertiSigmaClient(_BaseClient):
    """Synchronous CertiSigma API client.

    Without api_key, only public endpoints work (verify, batch_verify, health).
    """

    def __init__(self, api_key: Optional[str] = None, base_url: str = DEFAULT_BASE_URL, timeout: float = 30.0):
        super().__init__(api_key, base_url, timeout)
        self._http = httpx.Client(headers=self._headers, timeout=timeout)

    def close(self):
        self._http.close()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    def attest(
        self,
        hash_hex: str,
        source: Optional[str] = None,
        extra_data: Optional[dict] = None,
        client_encrypted: bool = False,
    ) -> AttestResult:
        """Create a new attestation for a SHA-256 hash. Requires api_key."""
        self._require_auth()
        payload = {"hash_hex": _validate_hash(hash_hex)}
        if source:
            payload["source"] = source
        if extra_data:
            payload["extra_data"] = extra_data
        if client_encrypted:
            payload["client_encrypted"] = True

        resp = self._http.post(f"{self._base_url}/attest", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return AttestResult(
            id=d["id"], hash_hex=d["hash_hex"], timestamp=d["timestamp"],
            status=d.get("status", "created"), signature=d.get("signature"),
            signing_key_id=d.get("signing_key_id"), format_version=d.get("format_version", "1.0.0"),
            claim_id=d.get("claim_id"), source=d.get("source"), extra_data=d.get("extra_data"),
            client_encrypted=d.get("client_encrypted", False),
        )

    def attest_file(
        self,
        file_path: str,
        source: Optional[str] = None,
        extra_data: Optional[dict] = None,
        client_encrypted: bool = False,
    ) -> AttestResult:
        """Hash a file with SHA-256 and create an attestation. Requires api_key."""
        self._require_auth()
        sha = hashlib.sha256()
        with open(file_path, "rb") as f:
            for chunk in iter(lambda: f.read(8192), b""):
                sha.update(chunk)
        return self.attest(sha.hexdigest(), source=source or file_path, extra_data=extra_data, client_encrypted=client_encrypted)

    def verify(self, hash_hex: str) -> VerifyResult:
        """Verify if a SHA-256 hash has been attested."""
        payload = {"hash_hex": _validate_hash(hash_hex)}
        resp = self._http.post(f"{self._base_url}/verify", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return VerifyResult(
            exists=d["exists"], hash_hex=d["hash_hex"], id=d.get("id"),
            timestamp=d.get("timestamp"), source=d.get("source"),
            level=d.get("level"), verified_at=d.get("verified_at"),
        )

    def batch_attest(
        self,
        hashes: list[str],
        source: Optional[str] = None,
        batch_id: Optional[str] = None,
        extra_data: Optional[dict] = None,
        client_encrypted: bool = False,
    ) -> BatchAttestResult:
        """Attest up to 100 hashes in a single call. Requires api_key."""
        self._require_auth()
        payload = {"hashes": [_validate_hash(h) for h in hashes]}
        if source:
            payload["source"] = source
        if batch_id:
            payload["batch_id"] = batch_id
        if extra_data:
            payload["extra_data"] = extra_data
        if client_encrypted:
            payload["client_encrypted"] = True

        resp = self._http.post(f"{self._base_url}/attest/batch", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return BatchAttestResult(
            batch_id=d["batch_id"], count=d["count"],
            created=d["created"], existing=d["existing"],
            attestations=d.get("attestations", []),
        )

    def batch_verify(self, hashes: list[str], detailed: bool = False) -> BatchVerifyResult:
        """Verify up to 100 hashes in a single call. When detailed=True, requires api_key and returns enriched results."""
        payload = {"hashes": [_validate_hash(h) for h in hashes]}
        if detailed:
            payload["detailed"] = True
        resp = self._http.post(f"{self._base_url}/verify/batch", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return BatchVerifyResult(
            count=d["count"], found=d["found"], not_found=d["not_found"],
            results=d.get("results", []),
        )

    def status(self, attestation_id: str) -> AttestationStatus:
        """Get full attestation status (T0/T1/T2 levels)."""
        resp = self._http.get(f"{self._base_url}/attestation/{attestation_id}/status")
        self._handle_error(resp)
        d = resp.json()
        return AttestationStatus(
            id=d["id"], hash_hex=d["hash_hex"], level=d["level"],
            t0_timestamp=d["t0_timestamp"], t1_timestamp=d.get("t1_timestamp"),
            t2_timestamp=d.get("t2_timestamp"), t2_bitcoin_block=d.get("t2_bitcoin_block"),
            signature_available=d.get("signature_available", False),
            tsa_available=d.get("tsa_available", False),
            merkle_proof_available=d.get("merkle_proof_available", False),
        )

    def update_metadata(
        self,
        attestation_id: int | str,
        source: Optional[str] = None,
        extra_data: Optional[dict] = None,
        client_encrypted: Optional[bool] = None,
    ) -> MetadataPatchResult:
        """Update claim metadata on an attestation. Requires api_key."""
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        payload: dict = {}
        if source is not None:
            payload["source"] = source
        if extra_data is not None:
            payload["extra_data"] = extra_data
        if client_encrypted is not None:
            payload["client_encrypted"] = client_encrypted
        resp = self._http.patch(f"{self._base_url}/attestation/{att_id}/metadata", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return MetadataPatchResult(
            success=d["success"], claim_id=d["claim_id"],
            attestation_id=d["attestation_id"], source=d.get("source"),
            extra_data=d.get("extra_data"), client_encrypted=d.get("client_encrypted", False),
            deleted=d.get("deleted", False), audit_entries=d.get("audit_entries", 0),
        )

    def delete_metadata(self, attestation_id: int | str) -> MetadataPatchResult:
        """Soft-delete claim metadata on an attestation. Requires api_key."""
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = self._http.patch(
            f"{self._base_url}/attestation/{att_id}/metadata",
            json={"delete": True},
        )
        self._handle_error(resp)
        d = resp.json()
        return MetadataPatchResult(
            success=d["success"], claim_id=d["claim_id"],
            attestation_id=d["attestation_id"], source=d.get("source"),
            extra_data=d.get("extra_data"), client_encrypted=d.get("client_encrypted", False),
            deleted=d.get("deleted", False), audit_entries=d.get("audit_entries", 0),
        )

    def get_evidence(self, attestation_id: int | str) -> EvidenceResult:
        """Get full cryptographic evidence for an attestation."""
        att_id = str(attestation_id).replace("att_", "")
        resp = self._http.get(f"{self._base_url}/attestation/{att_id}/evidence")
        self._handle_error(resp)
        d = resp.json()
        return EvidenceResult(
            hash_hex=d["hash_hex"], level=d["level"],
            t0=d.get("t0", {}), t1=d.get("t1"), t2=d.get("t2"),
            id=d.get("id"),
        )

    def get_metadata(self, attestation_id: int | str) -> MetadataResult:
        """Read claim metadata for an attestation. Requires api_key with metadata:read scope."""
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = self._http.get(f"{self._base_url}/attestation/{att_id}/metadata")
        self._handle_error(resp)
        d = resp.json()
        return MetadataResult(
            attestation_id=d["attestation_id"], source=d.get("source"),
            extra_data=d.get("extra_data"), client_encrypted=d.get("client_encrypted", False),
            created_at=d.get("created_at"), updated_at=d.get("updated_at"),
        )

    def create_share_token(
        self,
        attestation_ids: list[int],
        expires_in: int = 86400,
        recipient_label: Optional[str] = None,
        max_uses: Optional[int] = None,
    ) -> ShareTokenResult:
        """Create a time-limited forensic share token. Requires api_key with share scope."""
        self._require_auth()
        payload: dict = {"attestation_ids": attestation_ids, "expires_in": expires_in}
        if recipient_label:
            payload["recipient_label"] = recipient_label
        if max_uses is not None:
            payload["max_uses"] = max_uses
        resp = self._http.post(f"{self._base_url}/share", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return ShareTokenResult(
            id=d["id"], share_token=d["share_token"],
            attestation_ids=d["attestation_ids"], scope=d.get("scope", "read_metadata"),
            recipient_label=d.get("recipient_label"), max_uses=d.get("max_uses"),
            expires_at=d["expires_at"], created_at=d["created_at"],
        )

    def list_share_tokens(self) -> list[ShareTokenInfo]:
        """List all share tokens created by this API key. Requires share scope."""
        self._require_auth()
        resp = self._http.get(f"{self._base_url}/share")
        self._handle_error(resp)
        return [
            ShareTokenInfo(
                id=t["id"], attestation_ids=t["attestation_ids"],
                scope=t.get("scope", "read_metadata"), recipient_label=t.get("recipient_label"),
                max_uses=t.get("max_uses"), use_count=t.get("use_count", 0),
                expires_at=t["expires_at"], revoked_at=t.get("revoked_at"),
                created_at=t["created_at"], active=t.get("active", False),
            )
            for t in resp.json()
        ]

    def revoke_share_token(self, token_id: str) -> None:
        """Revoke a share token. Requires share scope."""
        self._require_auth()
        resp = self._http.delete(f"{self._base_url}/share/{token_id}")
        self._handle_error(resp)

    def get_share_token_info(self, token_id: str) -> ShareTokenInfo:
        """Get share token status. Requires share scope."""
        self._require_auth()
        resp = self._http.get(f"{self._base_url}/share/{token_id}/info")
        self._handle_error(resp)
        d = resp.json()
        return ShareTokenInfo(
            id=d["id"], attestation_ids=d["attestation_ids"],
            scope=d.get("scope", "read_metadata"), recipient_label=d.get("recipient_label"),
            max_uses=d.get("max_uses"), use_count=d.get("use_count", 0),
            expires_at=d["expires_at"], revoked_at=d.get("revoked_at"),
            created_at=d["created_at"], active=d.get("active", False),
        )

    def put_tags(
        self,
        attestation_id: int | str,
        tags: list[dict],
    ) -> TagResult:
        """Upsert tags on an attestation. Requires tags:write scope."""
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = self._http.put(
            f"{self._base_url}/attestation/{att_id}/tags",
            json={"tags": tags},
        )
        self._handle_error(resp)
        d = resp.json()
        return TagResult(attestation_id=d["attestation_id"], tags_upserted=d["tags_upserted"])

    def get_tags(self, attestation_id: int | str) -> list[Tag]:
        """List tags on an attestation. Requires tags:read scope."""
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = self._http.get(f"{self._base_url}/attestation/{att_id}/tags")
        self._handle_error(resp)
        return [
            Tag(
                key=t["key"], value=t.get("value"),
                value_enc=t.get("value_enc"), value_nonce=t.get("value_nonce"),
                client_encrypted=t.get("client_encrypted", False),
                created_at=t.get("created_at"), updated_at=t.get("updated_at"),
            )
            for t in resp.json()
        ]

    def delete_tag(self, attestation_id: int | str, tag_key: str) -> None:
        """Delete a specific tag. Requires tags:write scope."""
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = self._http.delete(f"{self._base_url}/attestation/{att_id}/tags/{tag_key}")
        self._handle_error(resp)

    def query_tags(self, filter: dict, limit: int = 100, cursor: Optional[str] = None) -> TagQueryResult:
        """Query attestations by tag combination. Requires tags:read scope."""
        self._require_auth()
        payload: dict = {"filter": filter, "limit": limit}
        if cursor:
            payload["cursor"] = cursor
        resp = self._http.post(f"{self._base_url}/tags/query", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return TagQueryResult(
            attestation_ids=d["attestation_ids"],
            next_cursor=d.get("next_cursor"),
            count=d["count"],
        )

    def create_derived_list(
        self,
        hashes: Optional[list[str]] = None,
        tag_filter: Optional[dict] = None,
        label: Optional[str] = None,
        expires_in_hours: Optional[int] = None,
    ) -> DerivedListResult:
        """Create an opaque derived list for third-party verification. Requires census scope + org_id."""
        self._require_auth()
        payload: dict = {}
        if hashes:
            payload["hashes"] = [_validate_hash(h) for h in hashes]
        if tag_filter:
            payload["tag_filter"] = tag_filter
        if label:
            payload["label"] = label
        if expires_in_hours is not None:
            payload["expires_in_hours"] = expires_in_hours
        resp = self._http.post(f"{self._base_url}/census/list", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return DerivedListResult(
            id=d["id"], list_key=d["list_key"], item_count=d["item_count"],
            content_hash=d["content_hash"], signature=d["signature"],
            signing_key_id=d["signing_key_id"], label=d.get("label"),
            expires_at=d.get("expires_at"), created_at=d.get("created_at", ""),
        )

    def list_derived_lists(self) -> list[DerivedListInfo]:
        """List your organization's derived lists. Requires census scope + org_id."""
        self._require_auth()
        resp = self._http.get(f"{self._base_url}/census/list")
        self._handle_error(resp)
        return [
            DerivedListInfo(
                id=dl["id"], item_count=dl["item_count"], label=dl.get("label"),
                expires_at=dl.get("expires_at"), revoked_at=dl.get("revoked_at"),
                created_at=dl.get("created_at", ""), active=dl.get("active", True),
            )
            for dl in resp.json()
        ]

    def get_derived_list(self, list_id: str) -> DerivedListDetail:
        """Get derived list details (owner only). Requires census scope + org_id."""
        self._require_auth()
        resp = self._http.get(f"{self._base_url}/census/list/{list_id}")
        self._handle_error(resp)
        d = resp.json()
        return DerivedListDetail(
            id=d["id"], item_count=d["item_count"], content_hash=d["content_hash"],
            signing_key_id=d["signing_key_id"], label=d.get("label"),
            tag_filter=d.get("tag_filter"), expires_at=d.get("expires_at"),
            revoked_at=d.get("revoked_at"), created_at=d.get("created_at", ""),
            active=d.get("active", True), match_count=d.get("match_count", 0),
        )

    def match_derived_list(self, list_id: str, list_key: str, hashes: list[str]) -> MatchResult:
        """Match derived hashes against a list (public — requires list_key)."""
        _validate_hash(list_key)
        resp = self._http.post(f"{self._base_url}/census/list/{list_id}/match", json={
            "list_key": list_key, "hashes": [_validate_hash(h) for h in hashes],
        })
        self._handle_error(resp)
        d = resp.json()
        return MatchResult(
            list_id=d["list_id"], matched=d["matched"],
            unmatched=d["unmatched"], total=d["total"],
            matches=d.get("matches", []),
        )

    def get_derived_list_signature(self, list_id: str) -> DerivedListSignature:
        """Get ECDSA signature of a derived list (public)."""
        resp = self._http.get(f"{self._base_url}/census/list/{list_id}/signature")
        self._handle_error(resp)
        d = resp.json()
        return DerivedListSignature(
            list_id=d["list_id"], signature=d["signature"],
            signing_key_id=d["signing_key_id"], content_hash=d["content_hash"],
            item_count=d["item_count"], created_at=d.get("created_at", ""),
            expires_at=d.get("expires_at"),
        )

    def get_derived_list_access_log(self, list_id: str, limit: int = 50) -> list[AccessLogEntry]:
        """View match audit trail for a derived list (owner only). Requires census scope + org_id."""
        self._require_auth()
        resp = self._http.get(f"{self._base_url}/census/list/{list_id}/access-log", params={"limit": limit})
        self._handle_error(resp)
        return [
            AccessLogEntry(
                matched_count=e["matched_count"], total_submitted=e["total_submitted"],
                ip_address=e.get("ip_address"), created_at=e.get("created_at", ""),
            )
            for e in resp.json()
        ]

    def revoke_derived_list(self, list_id: str) -> dict:
        """Revoke a derived list. Requires census scope + org_id."""
        self._require_auth()
        resp = self._http.delete(f"{self._base_url}/census/list/{list_id}")
        self._handle_error(resp)
        return resp.json()

    def health(self) -> dict:
        """Check API health."""
        resp = self._http.get(f"{self._base_url}/healthz")
        self._handle_error(resp)
        return resp.json()


class AsyncCertiSigmaClient(_BaseClient):
    """Asynchronous CertiSigma API client.

    Without api_key, only public endpoints work (verify, batch_verify, health).
    """

    def __init__(self, api_key: Optional[str] = None, base_url: str = DEFAULT_BASE_URL, timeout: float = 30.0):
        super().__init__(api_key, base_url, timeout)
        self._http = httpx.AsyncClient(headers=self._headers, timeout=timeout)

    async def close(self):
        await self._http.aclose()

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.close()

    async def attest(
        self,
        hash_hex: str,
        source: Optional[str] = None,
        extra_data: Optional[dict] = None,
        client_encrypted: bool = False,
    ) -> AttestResult:
        self._require_auth()
        payload = {"hash_hex": _validate_hash(hash_hex)}
        if source:
            payload["source"] = source
        if extra_data:
            payload["extra_data"] = extra_data
        if client_encrypted:
            payload["client_encrypted"] = True

        resp = await self._http.post(f"{self._base_url}/attest", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return AttestResult(
            id=d["id"], hash_hex=d["hash_hex"], timestamp=d["timestamp"],
            status=d.get("status", "created"), signature=d.get("signature"),
            signing_key_id=d.get("signing_key_id"), format_version=d.get("format_version", "1.0.0"),
            claim_id=d.get("claim_id"), source=d.get("source"), extra_data=d.get("extra_data"),
            client_encrypted=d.get("client_encrypted", False),
        )

    async def verify(self, hash_hex: str) -> VerifyResult:
        payload = {"hash_hex": _validate_hash(hash_hex)}
        resp = await self._http.post(f"{self._base_url}/verify", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return VerifyResult(
            exists=d["exists"], hash_hex=d["hash_hex"], id=d.get("id"),
            timestamp=d.get("timestamp"), source=d.get("source"),
            level=d.get("level"), verified_at=d.get("verified_at"),
        )

    async def batch_attest(
        self,
        hashes: list[str],
        source: Optional[str] = None,
        batch_id: Optional[str] = None,
        extra_data: Optional[dict] = None,
        client_encrypted: bool = False,
    ) -> BatchAttestResult:
        self._require_auth()
        payload = {"hashes": [_validate_hash(h) for h in hashes]}
        if source:
            payload["source"] = source
        if batch_id:
            payload["batch_id"] = batch_id
        if extra_data:
            payload["extra_data"] = extra_data
        if client_encrypted:
            payload["client_encrypted"] = True

        resp = await self._http.post(f"{self._base_url}/attest/batch", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return BatchAttestResult(
            batch_id=d["batch_id"], count=d["count"],
            created=d["created"], existing=d["existing"],
            attestations=d.get("attestations", []),
        )

    async def batch_verify(self, hashes: list[str], detailed: bool = False) -> BatchVerifyResult:
        payload = {"hashes": [_validate_hash(h) for h in hashes]}
        if detailed:
            payload["detailed"] = True
        resp = await self._http.post(f"{self._base_url}/verify/batch", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return BatchVerifyResult(
            count=d["count"], found=d["found"], not_found=d["not_found"],
            results=d.get("results", []),
        )

    async def status(self, attestation_id: str) -> AttestationStatus:
        resp = await self._http.get(f"{self._base_url}/attestation/{attestation_id}/status")
        self._handle_error(resp)
        d = resp.json()
        return AttestationStatus(
            id=d["id"], hash_hex=d["hash_hex"], level=d["level"],
            t0_timestamp=d["t0_timestamp"], t1_timestamp=d.get("t1_timestamp"),
            t2_timestamp=d.get("t2_timestamp"), t2_bitcoin_block=d.get("t2_bitcoin_block"),
            signature_available=d.get("signature_available", False),
            tsa_available=d.get("tsa_available", False),
            merkle_proof_available=d.get("merkle_proof_available", False),
        )

    async def update_metadata(
        self,
        attestation_id: int | str,
        source: Optional[str] = None,
        extra_data: Optional[dict] = None,
        client_encrypted: Optional[bool] = None,
    ) -> MetadataPatchResult:
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        payload: dict = {}
        if source is not None:
            payload["source"] = source
        if extra_data is not None:
            payload["extra_data"] = extra_data
        if client_encrypted is not None:
            payload["client_encrypted"] = client_encrypted
        resp = await self._http.patch(f"{self._base_url}/attestation/{att_id}/metadata", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return MetadataPatchResult(
            success=d["success"], claim_id=d["claim_id"],
            attestation_id=d["attestation_id"], source=d.get("source"),
            extra_data=d.get("extra_data"), client_encrypted=d.get("client_encrypted", False),
            deleted=d.get("deleted", False), audit_entries=d.get("audit_entries", 0),
        )

    async def delete_metadata(self, attestation_id: int | str) -> MetadataPatchResult:
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = await self._http.patch(
            f"{self._base_url}/attestation/{att_id}/metadata",
            json={"delete": True},
        )
        self._handle_error(resp)
        d = resp.json()
        return MetadataPatchResult(
            success=d["success"], claim_id=d["claim_id"],
            attestation_id=d["attestation_id"], source=d.get("source"),
            extra_data=d.get("extra_data"), client_encrypted=d.get("client_encrypted", False),
            deleted=d.get("deleted", False), audit_entries=d.get("audit_entries", 0),
        )

    async def get_evidence(self, attestation_id: int | str) -> EvidenceResult:
        att_id = str(attestation_id).replace("att_", "")
        resp = await self._http.get(f"{self._base_url}/attestation/{att_id}/evidence")
        self._handle_error(resp)
        d = resp.json()
        return EvidenceResult(
            hash_hex=d["hash_hex"], level=d["level"],
            t0=d.get("t0", {}), t1=d.get("t1"), t2=d.get("t2"),
            id=d.get("id"),
        )

    async def get_metadata(self, attestation_id: int | str) -> MetadataResult:
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = await self._http.get(f"{self._base_url}/attestation/{att_id}/metadata")
        self._handle_error(resp)
        d = resp.json()
        return MetadataResult(
            attestation_id=d["attestation_id"], source=d.get("source"),
            extra_data=d.get("extra_data"), client_encrypted=d.get("client_encrypted", False),
            created_at=d.get("created_at"), updated_at=d.get("updated_at"),
        )

    async def create_share_token(
        self, attestation_ids: list[int], expires_in: int = 86400,
        recipient_label: Optional[str] = None, max_uses: Optional[int] = None,
    ) -> ShareTokenResult:
        self._require_auth()
        payload: dict = {"attestation_ids": attestation_ids, "expires_in": expires_in}
        if recipient_label:
            payload["recipient_label"] = recipient_label
        if max_uses is not None:
            payload["max_uses"] = max_uses
        resp = await self._http.post(f"{self._base_url}/share", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return ShareTokenResult(
            id=d["id"], share_token=d["share_token"],
            attestation_ids=d["attestation_ids"], scope=d.get("scope", "read_metadata"),
            recipient_label=d.get("recipient_label"), max_uses=d.get("max_uses"),
            expires_at=d["expires_at"], created_at=d["created_at"],
        )

    async def list_share_tokens(self) -> list[ShareTokenInfo]:
        self._require_auth()
        resp = await self._http.get(f"{self._base_url}/share")
        self._handle_error(resp)
        return [
            ShareTokenInfo(
                id=t["id"], attestation_ids=t["attestation_ids"],
                scope=t.get("scope", "read_metadata"), recipient_label=t.get("recipient_label"),
                max_uses=t.get("max_uses"), use_count=t.get("use_count", 0),
                expires_at=t["expires_at"], revoked_at=t.get("revoked_at"),
                created_at=t["created_at"], active=t.get("active", False),
            )
            for t in resp.json()
        ]

    async def revoke_share_token(self, token_id: str) -> None:
        self._require_auth()
        resp = await self._http.delete(f"{self._base_url}/share/{token_id}")
        self._handle_error(resp)

    async def get_share_token_info(self, token_id: str) -> ShareTokenInfo:
        self._require_auth()
        resp = await self._http.get(f"{self._base_url}/share/{token_id}/info")
        self._handle_error(resp)
        d = resp.json()
        return ShareTokenInfo(
            id=d["id"], attestation_ids=d["attestation_ids"],
            scope=d.get("scope", "read_metadata"), recipient_label=d.get("recipient_label"),
            max_uses=d.get("max_uses"), use_count=d.get("use_count", 0),
            expires_at=d["expires_at"], revoked_at=d.get("revoked_at"),
            created_at=d["created_at"], active=d.get("active", False),
        )

    async def put_tags(self, attestation_id: int | str, tags: list[dict]) -> TagResult:
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = await self._http.put(
            f"{self._base_url}/attestation/{att_id}/tags", json={"tags": tags},
        )
        self._handle_error(resp)
        d = resp.json()
        return TagResult(attestation_id=d["attestation_id"], tags_upserted=d["tags_upserted"])

    async def get_tags(self, attestation_id: int | str) -> list[Tag]:
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = await self._http.get(f"{self._base_url}/attestation/{att_id}/tags")
        self._handle_error(resp)
        return [
            Tag(
                key=t["key"], value=t.get("value"),
                value_enc=t.get("value_enc"), value_nonce=t.get("value_nonce"),
                client_encrypted=t.get("client_encrypted", False),
                created_at=t.get("created_at"), updated_at=t.get("updated_at"),
            )
            for t in resp.json()
        ]

    async def delete_tag(self, attestation_id: int | str, tag_key: str) -> None:
        self._require_auth()
        att_id = str(attestation_id).replace("att_", "")
        resp = await self._http.delete(f"{self._base_url}/attestation/{att_id}/tags/{tag_key}")
        self._handle_error(resp)

    async def query_tags(self, filter: dict, limit: int = 100, cursor: Optional[str] = None) -> TagQueryResult:
        self._require_auth()
        payload: dict = {"filter": filter, "limit": limit}
        if cursor:
            payload["cursor"] = cursor
        resp = await self._http.post(f"{self._base_url}/tags/query", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return TagQueryResult(
            attestation_ids=d["attestation_ids"],
            next_cursor=d.get("next_cursor"),
            count=d["count"],
        )

    async def create_derived_list(
        self,
        hashes: Optional[list[str]] = None,
        tag_filter: Optional[dict] = None,
        label: Optional[str] = None,
        expires_in_hours: Optional[int] = None,
    ) -> DerivedListResult:
        self._require_auth()
        payload: dict = {}
        if hashes:
            payload["hashes"] = [_validate_hash(h) for h in hashes]
        if tag_filter:
            payload["tag_filter"] = tag_filter
        if label:
            payload["label"] = label
        if expires_in_hours is not None:
            payload["expires_in_hours"] = expires_in_hours
        resp = await self._http.post(f"{self._base_url}/census/list", json=payload)
        self._handle_error(resp)
        d = resp.json()
        return DerivedListResult(
            id=d["id"], list_key=d["list_key"], item_count=d["item_count"],
            content_hash=d["content_hash"], signature=d["signature"],
            signing_key_id=d["signing_key_id"], label=d.get("label"),
            expires_at=d.get("expires_at"), created_at=d.get("created_at", ""),
        )

    async def list_derived_lists(self) -> list[DerivedListInfo]:
        self._require_auth()
        resp = await self._http.get(f"{self._base_url}/census/list")
        self._handle_error(resp)
        return [
            DerivedListInfo(
                id=dl["id"], item_count=dl["item_count"], label=dl.get("label"),
                expires_at=dl.get("expires_at"), revoked_at=dl.get("revoked_at"),
                created_at=dl.get("created_at", ""), active=dl.get("active", True),
            )
            for dl in resp.json()
        ]

    async def get_derived_list(self, list_id: str) -> DerivedListDetail:
        self._require_auth()
        resp = await self._http.get(f"{self._base_url}/census/list/{list_id}")
        self._handle_error(resp)
        d = resp.json()
        return DerivedListDetail(
            id=d["id"], item_count=d["item_count"], content_hash=d["content_hash"],
            signing_key_id=d["signing_key_id"], label=d.get("label"),
            tag_filter=d.get("tag_filter"), expires_at=d.get("expires_at"),
            revoked_at=d.get("revoked_at"), created_at=d.get("created_at", ""),
            active=d.get("active", True), match_count=d.get("match_count", 0),
        )

    async def match_derived_list(self, list_id: str, list_key: str, hashes: list[str]) -> MatchResult:
        _validate_hash(list_key)
        resp = await self._http.post(f"{self._base_url}/census/list/{list_id}/match", json={
            "list_key": list_key, "hashes": [_validate_hash(h) for h in hashes],
        })
        self._handle_error(resp)
        d = resp.json()
        return MatchResult(
            list_id=d["list_id"], matched=d["matched"],
            unmatched=d["unmatched"], total=d["total"],
            matches=d.get("matches", []),
        )

    async def get_derived_list_signature(self, list_id: str) -> DerivedListSignature:
        resp = await self._http.get(f"{self._base_url}/census/list/{list_id}/signature")
        self._handle_error(resp)
        d = resp.json()
        return DerivedListSignature(
            list_id=d["list_id"], signature=d["signature"],
            signing_key_id=d["signing_key_id"], content_hash=d["content_hash"],
            item_count=d["item_count"], created_at=d.get("created_at", ""),
            expires_at=d.get("expires_at"),
        )

    async def get_derived_list_access_log(self, list_id: str, limit: int = 50) -> list[AccessLogEntry]:
        self._require_auth()
        resp = await self._http.get(f"{self._base_url}/census/list/{list_id}/access-log", params={"limit": limit})
        self._handle_error(resp)
        return [
            AccessLogEntry(
                matched_count=e["matched_count"], total_submitted=e["total_submitted"],
                ip_address=e.get("ip_address"), created_at=e.get("created_at", ""),
            )
            for e in resp.json()
        ]

    async def revoke_derived_list(self, list_id: str) -> dict:
        self._require_auth()
        resp = await self._http.delete(f"{self._base_url}/census/list/{list_id}")
        self._handle_error(resp)
        return resp.json()

    async def health(self) -> dict:
        resp = await self._http.get(f"{self._base_url}/healthz")
        self._handle_error(resp)
        return resp.json()
