"""Transport for the Core → Cloud membership projection (Workstream E4).

Transport only. The payload is the Workstream E schema-v1 DTO exactly as the projection
builder produced it. This module never recomputes roles, permissions, owner state, or the
legacy role. It mints a narrow capability and PUTs the payload.

A Cloud failure is returned as a WriteResult and never raised, so the caller can record it as
pending drift. Core authorization stays committed either way.
"""

from __future__ import annotations

import time
from dataclasses import dataclass
from urllib.parse import quote
from uuid import uuid4

import httpx
from jose import jwt

CORE_ISSUER = "p2hs-core"
CORE_AUDIENCE = "p2hs-cloud-auth-projection"
WRITE_CAPABILITY = "auth_projection.write"
CAPABILITY_TTL_SECONDS = 120
_MIN_SECRET_LENGTH = 32


@dataclass(frozen=True)
class WriteResult:
    ok: bool
    status: int | None
    code: str | None
    changed: bool | None = None


def mint_projection_capability(
    project_id: str,
    secret: str,
    *,
    now: float | None = None,
    ttl_seconds: int = CAPABILITY_TTL_SECONDS,
) -> str:
    """A capability bound to one project and one capability name, valid for a short window."""
    if len(secret) < _MIN_SECRET_LENGTH:
        raise ValueError("auth_projection_secret_too_short")
    issued = int(now if now is not None else time.time())
    claims = {
        "iss": CORE_ISSUER,
        "aud": CORE_AUDIENCE,
        "capability": WRITE_CAPABILITY,
        "project_id": project_id,
        "iat": issued,
        "exp": issued + ttl_seconds,
        "jti": uuid4().hex,
    }
    return jwt.encode(claims, secret, algorithm="HS256")


class CloudAuthProjectionWriter:
    def __init__(
        self,
        base_url: str,
        secret: str,
        *,
        transport: httpx.AsyncBaseTransport | None = None,
        timeout_seconds: float = 10.0,
    ) -> None:
        if not base_url:
            raise ValueError("cloud_control_api_url_not_configured")
        self._base_url = base_url.rstrip("/")
        self._secret = secret
        self._transport = transport
        self._timeout = timeout_seconds

    def projection_url(self, project_id: str, cloud_app_user_id: str) -> str:
        return (
            f"{self._base_url}/v1/cloud/core/projects/{quote(project_id, safe='')}"
            f"/auth/users/{quote(cloud_app_user_id, safe='')}/projection"
        )

    async def put_projection(
        self, project_id: str, cloud_app_user_id: str, payload: dict
    ) -> WriteResult:
        token = mint_projection_capability(project_id, self._secret)
        try:
            async with httpx.AsyncClient(
                transport=self._transport, timeout=self._timeout
            ) as client:
                response = await client.put(
                    self.projection_url(project_id, cloud_app_user_id),
                    json=payload,
                    headers={"Authorization": f"Bearer {token}"},
                )
        except httpx.HTTPError:
            return WriteResult(ok=False, status=None, code="transport_error")

        if response.status_code == 200:
            try:
                changed = bool(response.json().get("changed"))
            except ValueError:
                return WriteResult(ok=False, status=200, code="invalid_response")
            return WriteResult(ok=True, status=200, code=None, changed=changed)

        code = f"http_{response.status_code}"
        try:
            body = response.json()
            if isinstance(body, dict) and isinstance(body.get("error"), str):
                code = body["error"]
        except ValueError:
            pass
        return WriteResult(ok=False, status=response.status_code, code=code)
