import base64
import binascii
import json
import time
import uuid
from dataclasses import dataclass
from typing import Any

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

from .config import Settings
from .errors import ComputerRuntimeError


@dataclass(frozen=True, slots=True)
class CapabilityClaims:
    scope: str
    request_id: str
    session_id: uuid.UUID | None
    workspace_id: uuid.UUID | None
    fencing_token: int | None
    jti: uuid.UUID
    expires_at: int


def _decode(value: str) -> bytes:
    try:
        return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
    except (ValueError, TypeError, binascii.Error) as error:
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_INVALID", "The computer capability is invalid.", 401
        ) from error


def _uuid_claim(payload: dict[str, Any], name: str) -> uuid.UUID | None:
    value = payload.get(name)
    if value is None:
        return None
    try:
        return uuid.UUID(str(value))
    except (ValueError, TypeError) as error:
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_INVALID", "The computer capability is invalid.", 401
        ) from error


def verify_computer_capability(
    token: str | None, *, settings: Settings, expected_scope: str, request_id: str,
    session_id: uuid.UUID | None = None, workspace_id: uuid.UUID | None = None,
    fencing_token: int | None = None,
) -> CapabilityClaims:
    raw_key = settings.read_capability_public_key()
    if not raw_key:
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_UNCONFIGURED",
            "Computer capability verification is unconfigured.",
            503,
        )
    parts = token.split(".") if token else []
    if len(parts) != 3:
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_REQUIRED", "A computer capability is required.", 401
        )
    try:
        signing_input = f"{parts[0]}.{parts[1]}".encode("ascii")
        Ed25519PublicKey.from_public_bytes(raw_key).verify(_decode(parts[2]), signing_input)
        header = json.loads(_decode(parts[0]))
        payload = json.loads(_decode(parts[1]))
    except (
        InvalidSignature, UnicodeDecodeError, UnicodeEncodeError, json.JSONDecodeError, ValueError,
    ) as error:
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_INVALID", "The computer capability is invalid.", 401
        ) from error
    if not isinstance(header, dict) or not isinstance(payload, dict):
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_INVALID", "The computer capability is invalid.", 401
        )
    now = int(time.time())
    iat, not_before, expires = payload.get("iat"), payload.get("nbf"), payload.get("exp")
    if any((
        header.get("alg") != "EdDSA",
        header.get("typ") != "HAYVA-CAP",
        header.get("v") != 1,
        header.get("kid") != settings.computer_capability_key_id,
        payload.get("iss") != "core-api",
        payload.get("aud") != "computer-agent",
        payload.get("scope") != expected_scope,
        payload.get("request_id") != request_id,
        not isinstance(iat, int),
        not isinstance(not_before, int),
        not isinstance(expires, int),
    )):
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_INVALID", "The computer capability is invalid.", 401
        )
    if (
        iat > now + 5 or not_before > now
        or expires <= now or expires <= iat or expires <= not_before or expires - iat > 60
    ):
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_EXPIRED", "The computer capability is expired.", 401
        )
    claimed_session = _uuid_claim(payload, "session_id")
    claimed_workspace = _uuid_claim(payload, "workspace_id")
    claimed_fence = payload.get("fencing_token")
    if claimed_fence is not None and (
        not isinstance(claimed_fence, int) or isinstance(claimed_fence, bool)
        or claimed_fence <= 0
    ):
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_INVALID", "The computer capability is invalid.", 401
        )
    try:
        jti = uuid.UUID(str(payload.get("jti")))
    except (ValueError, TypeError) as error:
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_INVALID", "The computer capability is invalid.", 401
        ) from error
    if (
        (session_id is not None and claimed_session != session_id)
        or (workspace_id is not None and claimed_workspace != workspace_id)
        or (fencing_token is not None and claimed_fence != fencing_token)
    ):
        raise ComputerRuntimeError(
            "COMPUTER_CAPABILITY_SCOPE_MISMATCH",
            "The computer capability does not authorize this operation.",
            403,
        )
    return CapabilityClaims(
        scope=expected_scope, request_id=request_id, session_id=claimed_session,
        workspace_id=claimed_workspace,
        fencing_token=claimed_fence if isinstance(claimed_fence, int) else None,
        jti=jti, expires_at=payload["exp"],
    )
