"""Authenticated envelopes for retained private dashboard state."""

from __future__ import annotations

import base64
import binascii
import json
import os

from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM


class PrivateStateEncryptionError(RuntimeError):
    """Private retained state could not be authenticated or decrypted."""


def decode_private_state_encryption_key(encoded: str) -> bytes:
    """Decode the independently injected 256-bit private-state key."""
    try:
        key = base64.b64decode(encoded, validate=True)
    except (binascii.Error, ValueError) as error:
        raise PrivateStateEncryptionError(
            "private state encryption key is invalid"
        ) from error
    if len(key) != 32:
        raise PrivateStateEncryptionError(
            "private state encryption key must decode to exactly 32 bytes"
        )
    return key


def private_state_encryption_key() -> bytes:
    return decode_private_state_encryption_key(
        os.getenv("STACKCHAIN_PRIVATE_STATE_ENCRYPTION_KEY", "")
    )


class PrivateStateCipher:
    """Seal JSON values with store-specific authenticated context."""

    def __init__(self, key: bytes, *, store: str):
        if not isinstance(key, bytes) or len(key) != 32:
            raise PrivateStateEncryptionError(
                "private state encryption requires exactly 32 key bytes"
            )
        if not store or "\0" in store:
            raise ValueError("private state store identity is invalid")
        self._cipher = AESGCM(key)
        self._store = store

    def _aad(self, binding: str) -> bytes:
        return f"stackchain:private-state:v1\0{self._store}\0{binding}".encode()

    def seal(self, value: object, *, binding: str = "singleton") -> str:
        plaintext = json.dumps(value, separators=(",", ":")).encode()
        nonce = os.urandom(12)
        sealed = nonce + self._cipher.encrypt(nonce, plaintext, self._aad(binding))
        return "v1:" + base64.urlsafe_b64encode(sealed).decode()

    def open(self, payload: str, *, binding: str = "singleton") -> tuple[object, bool]:
        """Return the decoded value and whether plaintext migration is required."""
        if not isinstance(payload, str):
            raise PrivateStateEncryptionError("private state could not be decrypted")
        if not payload.startswith("v1:"):
            try:
                return json.loads(payload), True
            except (TypeError, json.JSONDecodeError) as error:
                raise PrivateStateEncryptionError(
                    "private state could not be decrypted"
                ) from error
        try:
            sealed = base64.b64decode(payload[3:], altchars=b"-_", validate=True)
            if len(sealed) < 28:
                raise ValueError("encrypted payload is too short")
            plaintext = self._cipher.decrypt(
                sealed[:12], sealed[12:], self._aad(binding)
            )
            return json.loads(plaintext), False
        except (
            binascii.Error,
            InvalidTag,
            UnicodeDecodeError,
            ValueError,
            json.JSONDecodeError,
        ) as error:
            raise PrivateStateEncryptionError(
                "private state could not be decrypted"
            ) from error
