# -*- coding: utf-8 -*-
#
# Copyright 2021 Nitrokey Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, at your option. This file may not be
# copied, modified, or distributed except according to those terms.
"""Python Library to manage NetHSM(s)."""

import binascii
import contextlib
import enum
import importlib.metadata
import json
import string
from base64 import b64decode, b64encode
from dataclasses import dataclass
from datetime import datetime, timezone
from io import BufferedReader, FileIO
from typing import TYPE_CHECKING, Any, Dict, Iterator, Mapping, NoReturn, Optional, TypeVar, Union
from urllib.parse import urlencode

import urllib3
from urllib3 import HTTPResponse, _collections

# Avoid direct imports from .client at runtime to reduce the module load time
if TYPE_CHECKING:
    from .client import ApiException
    from .client.apis.tags.default_api import DefaultApi
    from .client.components.schema.cluster_initial_member import ClusterInitialMemberDict
    from .client.components.schema.cluster_member import ClusterMemberDict
    from .client.components.schema.cluster_member_add_response import ClusterMemberAddResponseDict
    from .client.components.schema.ipv6_config import Ipv6ConfigDict
    from .client.components.schema.network_config_output import NetworkConfigOutputDict
    from .client.schemas import Unset


__version__ = importlib.metadata.version("nethsm")


Bytes = Union[BufferedReader, FileIO, bytes]


class Role(enum.Enum):
    ADMINISTRATOR = "Administrator"
    OPERATOR = "Operator"
    METRICS = "Metrics"
    BACKUP = "Backup"

    @staticmethod
    def from_string(s: str) -> "Role":
        for role in Role:
            if role.value == s:
                return role
        raise ValueError(f"Unsupported user role {s}")


class State(enum.Enum):
    UNPROVISIONED = "Unprovisioned"
    LOCKED = "Locked"
    OPERATIONAL = "Operational"

    @staticmethod
    def from_string(s: str) -> "State":
        for state in State:
            if state.value == s:
                return state
        raise ValueError(f"Unsupported system state {s}")


class LogLevel(enum.Enum):
    DEBUG = "debug"
    INFO = "info"
    WARNING = "warning"
    ERROR = "error"

    @staticmethod
    def from_string(s: str) -> "LogLevel":
        for log_level in LogLevel:
            if log_level.value == s:
                return log_level
        raise ValueError(f"Unsupported log level {s}")


class UnattendedBootStatus(enum.Enum):
    ON = "on"
    OFF = "off"

    @staticmethod
    def from_string(s: str) -> "UnattendedBootStatus":
        for status in UnattendedBootStatus:
            if status.value == s:
                return status
        raise ValueError(f"Unsupported unattended boot status {s}")


class KeyType(enum.Enum):
    RSA = "RSA"
    CURVE25519 = "Curve25519"
    EC_P256 = "EC_P256"
    EC_P384 = "EC_P384"
    EC_P521 = "EC_P521"
    EC_P256K1 = "EC_P256K1"
    BRAINPOOL_P256 = "BrainpoolP256"
    BRAINPOOL_P384 = "BrainpoolP384"
    BRAINPOOL_P512 = "BrainpoolP512"
    GENERIC = "Generic"

    @staticmethod
    def from_string(s: str) -> "KeyType":
        for key_type in KeyType:
            if key_type.value == s:
                return key_type
        raise ValueError(f"Unsupported key type {s}")


class KeyMechanism(enum.Enum):
    RSA_DECRYPTION_RAW = "RSA_Decryption_RAW"
    RSA_DECRYPTION_PKCS1 = "RSA_Decryption_PKCS1"
    RSA_DECRYPTION_OAEP_MD5 = "RSA_Decryption_OAEP_MD5"
    RSA_DECRYPTION_OAEP_SHA1 = "RSA_Decryption_OAEP_SHA1"
    RSA_DECRYPTION_OAEP_SHA224 = "RSA_Decryption_OAEP_SHA224"
    RSA_DECRYPTION_OAEP_SHA256 = "RSA_Decryption_OAEP_SHA256"
    RSA_DECRYPTION_OAEP_SHA384 = "RSA_Decryption_OAEP_SHA384"
    RSA_DECRYPTION_OAEP_SHA512 = "RSA_Decryption_OAEP_SHA512"
    RSA_SIGNATURE_PKCS1 = "RSA_Signature_PKCS1"
    RSA_SIGNATURE_PSS_MD5 = "RSA_Signature_PSS_MD5"
    RSA_SIGNATURE_PSS_SHA1 = "RSA_Signature_PSS_SHA1"
    RSA_SIGNATURE_PSS_SHA224 = "RSA_Signature_PSS_SHA224"
    RSA_SIGNATURE_PSS_SHA256 = "RSA_Signature_PSS_SHA256"
    RSA_SIGNATURE_PSS_SHA384 = "RSA_Signature_PSS_SHA384"
    RSA_SIGNATURE_PSS_SHA512 = "RSA_Signature_PSS_SHA512"
    EDDSA_SIGNATURE = "EdDSA_Signature"
    ECDSA_SIGNATURE = "ECDSA_Signature"
    BIP340_SIGNATURE = "BIP340_Signature"
    AES_ENCRYPTION_CBC = "AES_Encryption_CBC"
    AES_DECRYPTION_CBC = "AES_Decryption_CBC"

    @staticmethod
    def from_string(s: str) -> "KeyMechanism":
        for key_mechanism in KeyMechanism:
            if key_mechanism.value == s:
                return key_mechanism
        raise ValueError(f"Unsupported key mechanism {s}")


class EncryptMode(enum.Enum):
    AES_CBC = "AES_CBC"

    @staticmethod
    def from_string(s: str) -> "EncryptMode":
        for mode in EncryptMode:
            if mode.value == s:
                return mode
        raise ValueError(f"Unsupported encrypt mode {s}")


class DecryptMode(enum.Enum):
    RAW = "RAW"
    PKCS1 = "PKCS1"
    OAEP_MD5 = "OAEP_MD5"
    OAEP_SHA1 = "OAEP_SHA1"
    OAEP_SHA224 = "OAEP_SHA224"
    OAEP_SHA256 = "OAEP_SHA256"
    OAEP_SHA384 = "OAEP_SHA384"
    OAEP_SHA512 = "OAEP_SHA512"
    AES_CBC = "AES_CBC"

    @staticmethod
    def from_string(s: str) -> "DecryptMode":
        for mode in DecryptMode:
            if mode.value == s:
                return mode
        raise ValueError(f"Unsupported decrypt mode {s}")


class SignMode(enum.Enum):
    PKCS1 = "PKCS1"
    PSS_MD5 = "PSS_MD5"
    PSS_SHA1 = "PSS_SHA1"
    PSS_SHA224 = "PSS_SHA224"
    PSS_SHA256 = "PSS_SHA256"
    PSS_SHA384 = "PSS_SHA384"
    PSS_SHA512 = "PSS_SHA512"
    EDDSA = "EdDSA"
    ECDSA = "ECDSA"
    BIP340 = "BIP340"

    @staticmethod
    def from_string(s: str) -> "SignMode":
        for mode in SignMode:
            if mode.value == s:
                return mode
        raise ValueError(f"Unsupported sign mode {s}")


class TlsKeyType(enum.Enum):
    RSA = "RSA"
    CURVE25519 = "Curve25519"
    EC_P256 = "EC_P256"
    EC_P384 = "EC_P384"
    EC_P521 = "EC_P521"
    BRAINPOOL_P256 = "BrainpoolP256"
    BRAINPOOL_P384 = "BrainpoolP384"
    BRAINPOOL_P512 = "BrainpoolP512"

    @staticmethod
    def from_string(s: str) -> "TlsKeyType":
        for key_type in TlsKeyType:
            if key_type.value == s:
                return key_type
        raise ValueError(f"Unsupported TLS key type {s}")


@dataclass
class Base64:
    data: str

    def decode(self) -> bytes:
        return b64decode(self.data)

    @classmethod
    def from_encoded(cls, data: Union[bytes, str], ignore_whitespace: bool = False) -> "Base64":
        """
        >>> Base64.from_encoded("dGVzdAo=")
        Base64(data='dGVzdAo=')
        >>> Base64.from_encoded(b"dGVzdAo=")
        Base64(data='dGVzdAo=')
        >>> try:
        ...     Base64.from_encoded("dGV zdAo=")
        ...     assert False
        ... except ValueError as e:
        ...     assert "Invalid base64 data" in str(e)
        >>> try:
        ...     Base64.from_encoded(b"dGV zdAo=")
        ...     assert False
        ... except ValueError as e:
        ...     assert "Invalid base64 data" in str(e)
        >>> Base64.from_encoded("dGV zdAo=", ignore_whitespace=True)
        Base64(data='dGVzdAo=')
        >>> Base64.from_encoded(b"dGV zdAo=", ignore_whitespace=True)
        Base64(data='dGVzdAo=')
        """
        if ignore_whitespace:
            if isinstance(data, bytes):
                data = data.translate(None, delete=string.whitespace.encode())
            else:
                data = data.translate(str.maketrans("", "", string.whitespace))

        try:
            b64decode(data, validate=True)
        except binascii.Error as e:
            if isinstance(data, bytes):
                data = data.decode(errors="replace")
            raise ValueError(f"Invalid base64 data: {e}: {data}") from None

        if isinstance(data, bytes):
            data = data.decode()
        return cls(data=data)

    @classmethod
    def encode(cls, data: bytes) -> "Base64":
        return cls(data=b64encode(data).decode())


@dataclass
class Authentication:
    username: str
    password: str


@dataclass
class Info:
    vendor: str
    product: str


@dataclass
class Tpm:
    attestation_keys: Dict[str, Any]
    platform_configuration_registers: Dict[str, Any]


@dataclass
class SystemInfo:
    firmware_version: str
    software_version: str
    hardware_version: str
    build_tag: str
    tpm: Tpm


@dataclass
class User:
    user_id: str
    real_name: str
    role: Role


@dataclass
class RsaPublicKey:
    modulus: Base64
    public_exponent: Base64


@dataclass
class EcPublicKey:
    data: Base64


PublicKey = Union[RsaPublicKey, EcPublicKey, None]


@dataclass
class RsaPrivateKey:
    prime_p: Base64
    prime_q: Base64
    public_exponent: Base64


@dataclass
class GenericPrivateKey:
    data: Base64


PrivateKey = Union[RsaPrivateKey, GenericPrivateKey]


@dataclass
class Key:
    key_id: str
    mechanisms: list[KeyMechanism]
    type: KeyType
    operations: int
    tags: list[str]
    public_key: PublicKey


@dataclass
class EncryptionResult:
    encrypted: Base64
    iv: Base64


@dataclass
class LoggingConfig:
    ip_address: str
    log_level: LogLevel
    port: int


@dataclass
class Ipv6Config:
    cidr: str
    gateway: Optional[str] = None

    def _to_api(self) -> "Ipv6ConfigDict":
        from .client.components.schema.ipv6_config import Ipv6ConfigDict

        return Ipv6ConfigDict(cidr=self.cidr, gateway=self.gateway)

    @staticmethod
    def _from_api(item: "Ipv6ConfigDict") -> "Ipv6Config":
        return Ipv6Config(cidr=item.cidr, gateway=item.gateway)


@dataclass
class NetworkConfig:
    gateway: str
    ip_address: str
    netmask: str
    ipv6: Optional[Ipv6Config] = None

    @staticmethod
    def _from_api(item: "NetworkConfigOutputDict") -> "NetworkConfig":
        ipv6 = _unset_to_optional(item.ipv6)
        return NetworkConfig(
            gateway=item.gateway,
            ip_address=item.ipAddress,
            netmask=item.netmask,
            ipv6=Ipv6Config._from_api(ipv6) if ipv6 is not None else None,
        )


@dataclass
class ClusterMember:
    id: str
    name: str
    urls: list[str]

    @staticmethod
    def _from_api(item: "ClusterMemberDict") -> "ClusterMember":
        return ClusterMember(id=item.id, name=item.name, urls=list(item.urls))


@dataclass
class InitialClusterMember:
    name: str
    urls: list[str]

    def to_dict(self) -> dict[str, object]:
        return {"name": self.name, "urls": self.urls}

    @staticmethod
    def from_dict(data: dict[object, object]) -> "InitialClusterMember":
        if "name" not in data:
            raise ValueError("Missing name field for initial cluster member")
        if "urls" not in data:
            raise ValueError("Missing urls field for initial cluster member")
        name = data["name"]
        urls = data["urls"]

        if not isinstance(name, str):
            raise ValueError("Name field for initial cluster member is not a string")
        if not isinstance(urls, list):
            raise ValueError("URLs field for initial cluster member is not a list")
        for url in urls:
            if not isinstance(url, str):
                raise ValueError("URL for initial cluster member is not a string")

        # ty cannot infer from our checks above that urls is a list[str]
        return InitialClusterMember(name=name, urls=urls)  # ty: ignore[invalid-argument-type]

    @staticmethod
    def _from_api(data: "ClusterInitialMemberDict") -> "InitialClusterMember":
        return InitialClusterMember(name=data.name, urls=list(data.urls))


@dataclass
class ClusterJoinData:
    members: list[InitialClusterMember]
    joiner_kit: str

    def to_dict(self) -> dict[str, object]:
        return {
            "members": [member.to_dict() for member in self.members],
            "joiner_kit": self.joiner_kit,
        }

    @staticmethod
    def from_dict(data: dict[object, object]) -> "ClusterJoinData":
        if "members" not in data:
            raise ValueError("Missing members field for cluster join data")
        if "joiner_kit" not in data:
            raise ValueError("Missing joiner_kit field for cluster join data")
        members = data["members"]
        joiner_kit = data["joiner_kit"]

        if not isinstance(members, list):
            raise ValueError("Members field for cluster join data is not a list")
        for member in members:
            if not isinstance(member, dict):
                raise ValueError("Member for cluster join data is not a dict")
        if not isinstance(joiner_kit, str):
            raise ValueError("Joiner kit field for cluster join data is not a string")

        return ClusterJoinData(
            members=[InitialClusterMember.from_dict(member) for member in members],  # ty: ignore[invalid-argument-type]
            joiner_kit=joiner_kit,
        )

    @staticmethod
    def _from_api(response: "ClusterMemberAddResponseDict") -> "ClusterJoinData":
        members = [InitialClusterMember._from_api(member) for member in response.members]
        return ClusterJoinData(members=members, joiner_kit=response.joinerKit)


def _handle_exception(
    e: Exception,
    messages: Optional[dict[int, str]] = None,
    roles: Optional[list[Role]] = None,
    state: Optional[State] = None,
) -> NoReturn:
    from .client import ApiException

    if isinstance(e, ApiException):
        _handle_api_exception(e, messages, roles, state)
    elif isinstance(e, urllib3.exceptions.MaxRetryError):
        if isinstance(e.reason, urllib3.exceptions.SSLError):
            raise NetHSMRequestError(RequestErrorType.SSL_ERROR, e)
        raise NetHSMRequestError(RequestErrorType.OTHER, e)
    else:
        raise e


def _handle_api_exception(
    e: "ApiException[Any]",
    messages: Optional[dict[int, str]] = None,
    roles: Optional[list[Role]] = None,
    state: Optional[State] = None,
) -> NoReturn:
    from .client.api_response import ApiResponseWithoutDeserialization
    from .client.components.schema.error_response import ErrorResponseDict
    from .client.schemas import Unset

    if messages is None:
        messages = {}

    if e.status in messages:
        message = messages[e.status]
    elif e.status == 401 and roles:
        message = "Unauthorized -- invalid username or password"
    elif e.status == 403 and roles:
        roles_str = [role.value for role in roles]
        message = "Access denied -- this operation requires the role " + " or ".join(roles_str)
    elif e.status == 405:
        # 405 "Method Not Allowed" mostly happens when the UserID or KeyID contains a character
        # - that ends the path of the URL like a question mark '?' :
        #   /api/v1/keys/?/cert will hit the keys listing endpoint instead of the key/{KeyID}/cert endpoint
        # - that doesn't count as a path parameter like a slash '/' :
        #   /api/v1/keys///cert will be interpreted as /api/v1/keys/cert with cert as the KeyID
        message = "The ID you provided contains invalid characters"
    elif e.status == 406:
        message = "Invalid content type requested"
    elif e.status == 412 and state:
        message = f"Precondition failed -- this operation can only be used on a NetHSM in the state {state.value}"
    elif e.status == 429:
        message = "Too many requests -- you may have tried the wrong credentials too often"
    else:
        message = f"Unexpected API error {e.status}: {e.reason}"

    if e.api_response:
        custom_message = None
        # Raw responses returned by the _request function or for skip_deserialization=True
        if isinstance(e.api_response, ApiResponseWithoutDeserialization):
            try:
                response = e.api_response.response.json()
                if "message" in response:
                    custom_message = response["message"]
            except json.JSONDecodeError:
                pass
        # Errors raised by the auto-generated client
        elif isinstance(e.api_response.body, ErrorResponseDict):
            if not isinstance(e.api_response.body.message, Unset):
                custom_message = e.api_response.body.message

        if custom_message is not None:
            message += "\nNetHSM error message: " + custom_message

    raise NetHSMError(message)


class NetHSMError(Exception):
    def __init__(self, message: str) -> None:
        super().__init__(message)


class RequestErrorType(enum.Enum):
    SSL_ERROR = "SSL_ERROR"
    OTHER = "OTHER"


class NetHSMRequestError(Exception):
    def __init__(self, type: RequestErrorType, reason: Exception) -> None:
        super().__init__(f"NetHSM API request error: {type.value} {reason}")
        self.type = type
        self.reason = reason


_T = TypeVar("_T")


def _optional_to_unset(value: Optional[_T]) -> Union[_T, "Unset"]:
    from .client.schemas import Unset

    if value is None:
        return Unset()
    return value


def _unset_to_optional(value: Union[_T, "Unset"]) -> Optional[_T]:
    from .client.schemas import Unset

    if isinstance(value, Unset):
        return None
    return value


class NetHSM:
    def __init__(
        self,
        host: str,
        auth: Optional[Authentication] = None,
        verify_tls: bool = True,
        ca_certs: Optional[str] = None,
    ) -> None:
        from .client import ApiClient, ApiConfiguration
        from .client.components.security_schemes import security_scheme_basic
        from .client.configurations.api_configuration import SecuritySchemeInfo, ServerInfo
        from .client.servers.server_0 import Server0, VariablesDict, Version

        version = Version.default

        self.host = host
        self.version = version
        self.auth = auth

        security_info = None
        if auth:
            security_info = SecuritySchemeInfo(
                {
                    "basic": security_scheme_basic.Basic(
                        user_id=auth.username, password=auth.password
                    )
                }
            )

        server_config = ServerInfo(
            {"servers/0": Server0(variables=VariablesDict(host=host, version=version))}
        )
        config = ApiConfiguration(server_info=server_config, security_scheme_info=security_info)
        config.ssl_ca_cert = ca_certs  # type: ignore[assignment]
        config.verify_ssl = verify_tls
        self.client = ApiClient(configuration=config)

        if not verify_tls:
            urllib3.disable_warnings()

    def close(self) -> None:
        self.client.close()  # type: ignore[no-untyped-call]

    def _request(
        self,
        method: str,
        endpoint: str,
        params: Optional[dict[str, str]] = None,
        data: Optional[Bytes] = None,
        mime_type: Optional[str] = "application/json",
        json_obj: Optional[Any] = None,
    ) -> HTTPResponse:
        from .client import ApiException
        from .client.api_response import ApiResponseWithoutDeserialization

        url = f"https://{self.host}/api/{self.version}/{endpoint}"

        if params:
            url += "?" + urlencode(params)

        headers = _collections.HTTPHeaderDict()
        if mime_type is not None:
            headers["Content-Type"] = mime_type

        # basic auth from self.auth
        if self.auth:
            headers["Authorization"] = (
                f"Basic {b64encode(f'{self.auth.username}:{self.auth.password}'.encode('latin-1')).decode()}"
            )

        body: Union[str, bytes, None] = None
        if data:
            if isinstance(data, bytes):
                body = data
            else:
                body = data.read()
        elif json_obj:
            encoder = json.JSONEncoder()
            body = encoder.encode(json_obj)

        response = self._get_api().api_client.rest_client.request(
            method=method, url=url, headers=headers, body=body
        )

        if 200 > response.status or response.status > 399:
            api_response = ApiResponseWithoutDeserialization(response=response)
            raise ApiException(
                status=response.status, reason=response.reason, api_response=api_response
            )
        return response

    def _get_create_resource_id(self, response: HTTPResponse) -> str:
        # Endpoints that return CreateResourceId cannot be deserialized because
        # they also set a header which our generator does not support.  We need
        # to deserialize them manually.
        data = response.json()
        assert "id" in data
        value = data["id"]
        assert isinstance(value, str)
        return value

    def _get_api(self) -> "DefaultApi":
        from .client.apis.tags.default_api import DefaultApi

        return DefaultApi(self.client)

    def unlock(self, passphrase: str) -> None:
        from .client.components.schema.unlock_request_data import UnlockRequestDataDict

        request_body = UnlockRequestDataDict(passphrase=passphrase)
        try:
            self._get_api().unlock_post(request_body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.LOCKED,
                messages={
                    # Doc says 400 could happen when the passphrase is invalid?
                    400: "Access denied -- wrong unlock passphrase",
                    403: "Access denied -- wrong unlock passphrase",
                },
            )

    def lock(self) -> None:
        try:
            self._get_api().lock_post()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def provision(
        self,
        unlock_passphrase: str,
        admin_passphrase: str,
        system_time: Optional[Union[str, datetime]] = None,
    ) -> None:
        from .client.components.schema.provision_request_data import ProvisionRequestDataDict

        if system_time is None:
            system_time = datetime.now(timezone.utc)

        request_body = ProvisionRequestDataDict(
            unlockPassphrase=unlock_passphrase,
            adminPassphrase=admin_passphrase,
            systemTime=system_time,
        )
        try:
            self._get_api().provision_post(request_body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.UNPROVISIONED,
                messages={400: "Malformed request data -- e. g. weak passphrase or invalid time"},
            )

    def list_users(self) -> list[str]:
        try:
            response = self._get_api().users_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return [item.user for item in response.body]

    def get_user(self, user_id: str) -> User:
        from .client.paths.users_user_id.get.path_parameters import PathParametersDict

        path_params = PathParametersDict(UserID=user_id)
        try:
            response = self._get_api().users_user_id_get(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR, Role.OPERATOR],
                messages={404: f"User {user_id} not found"},
            )
        return User(
            user_id=user_id,
            real_name=response.body.realName,
            role=Role.from_string(response.body.role),
        )

    def add_user(
        self,
        real_name: str,
        role: Role,
        passphrase: str,
        user_id: Optional[str] = None,
        namespace: Optional[str] = None,
    ) -> str:
        from .client.components.schema.user_post_data import UserPostDataDict

        if namespace:
            if user_id:
                user_id = f"{namespace}~{user_id}"
            else:
                namespace = namespace + "~"

        body = UserPostDataDict(realName=real_name, role=role.value, passphrase=passphrase)
        try:
            if user_id:
                from .client.paths.users_user_id.put.path_parameters import (
                    PathParametersDict as PutParameters,
                )

                put_path_params = PutParameters(UserID=user_id)
                self._get_api().users_user_id_put(path_params=put_path_params, body=body)
            elif namespace:
                from .client.paths.users_user_id.post.path_parameters import (
                    PathParametersDict as PostParameters,
                )

                post_path_params = PostParameters(UserID=namespace)
                response = self._get_api().users_user_id_post(
                    path_params=post_path_params, body=body, skip_deserialization=True
                )
                user_id = self._get_create_resource_id(response.response)
            else:
                response = self._get_api().users_post(body=body, skip_deserialization=True)
                user_id = self._get_create_resource_id(response.response)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    400: "Bad request -- e. g. weak passphrase",
                    409: f"Conflict -- a user with the ID {user_id} already exists",
                },
            )
        return user_id

    def delete_user(self, user_id: str) -> None:
        from .client.paths.users_user_id.delete.path_parameters import PathParametersDict

        try:
            path_params = PathParametersDict(UserID=user_id)
            self._get_api().users_user_id_delete(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={404: f"User {user_id} not found"},
            )

    def set_passphrase(self, user_id: str, passphrase: str) -> None:
        from .client.components.schema.user_passphrase_post_data import UserPassphrasePostDataDict
        from .client.paths.users_user_id_passphrase.post.path_parameters import PathParametersDict

        path_params = PathParametersDict(UserID=user_id)
        body = UserPassphrasePostDataDict(passphrase=passphrase)
        try:
            self._get_api().users_user_id_passphrase_post(path_params=path_params, body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR, Role.OPERATOR],
                messages={
                    400: "Bad request -- e. g. weak passphrase",
                    404: f"User {user_id} not found",
                },
            )

    def list_namespaces(self) -> list[str]:
        try:
            response = self._get_api().namespaces_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return [item.id for item in response.body]

    def add_namespace(self, namespace: str) -> None:
        from .client.paths.namespaces_namespace_id.put.path_parameters import PathParametersDict

        path_params = PathParametersDict(NamespaceID=namespace)
        try:
            self._get_api().namespaces_namespace_id_put(path_params=path_params)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def delete_namespace(self, namespace: str) -> None:
        from .client.paths.namespaces_namespace_id.delete.path_parameters import PathParametersDict

        path_params = PathParametersDict(NamespaceID=namespace)
        try:
            self._get_api().namespaces_namespace_id_delete(path_params=path_params)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def list_operator_tags(self, user_id: str) -> list[str]:
        from .client.paths.users_user_id_tags.get.path_parameters import PathParametersDict

        path_params = PathParametersDict(UserID=user_id)
        try:
            response = self._get_api().users_user_id_tags_get(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={404: f"User {user_id} not found"},
            )
        return list(response.body)

    def add_operator_tag(self, user_id: str, tag: str) -> None:
        from .client.paths.users_user_id_tags_tag.put.path_parameters import PathParametersDict

        path_params = PathParametersDict(UserID=user_id, Tag=tag)
        try:
            self._get_api().users_user_id_tags_tag_put(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    304: f"Tag is already present for {user_id}",
                    400: "Invalid tag format or user is not an operator",
                    404: f"User {user_id} not found",
                },
            )

    def delete_operator_tag(self, user_id: str, tag: str) -> None:
        from .client.paths.users_user_id_tags_tag.delete.path_parameters import PathParametersDict

        path_params = PathParametersDict(UserID=user_id, Tag=tag)
        try:
            self._get_api().users_user_id_tags_tag_delete(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={404: f"User {user_id} or tag {tag} not found"},
            )

    def add_key_tag(self, key_id: str, tag: str) -> None:
        from .client.paths.keys_key_id_restrictions_tags_tag.put.path_parameters import (
            PathParametersDict,
        )

        path_params = PathParametersDict(KeyID=key_id, Tag=tag)
        try:
            self._get_api().keys_key_id_restrictions_tags_tag_put(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    304: f"Tag is already present for {key_id}",
                    400: f"Tag {tag} has invalid format",
                    404: f"Key {key_id} not found",
                },
            )

    def delete_key_tag(self, key_id: str, tag: str) -> None:
        from .client.paths.keys_key_id_restrictions_tags_tag.delete.path_parameters import (
            PathParametersDict,
        )

        path_params = PathParametersDict(KeyID=key_id, Tag=tag)
        try:
            self._get_api().keys_key_id_restrictions_tags_tag_delete(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={404: f"Key {key_id} or tag {tag} not found"},
            )

    def get_info(self) -> Info:
        try:
            response = self._get_api().info_get()
        except Exception as e:
            _handle_exception(e)
        return Info(vendor=response.body.vendor, product=response.body.product)

    def get_state(self) -> State:
        try:
            response = self._get_api().health_state_get()
        except Exception as e:
            _handle_exception(e)
        return State.from_string(response.body.state)

    def get_random_data(self, n: int) -> Base64:
        from .client.components.schema.random_request_data import RandomRequestDataDict

        body = RandomRequestDataDict(length=n)
        try:
            response = self._get_api().random_post(body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.OPERATOR],
                messages={400: "Invalid length. Must be between 1 and 1024"},
            )
        return Base64.from_encoded(response.body.random)

    def get_metrics(self) -> Mapping[str, Any]:
        try:
            response = self._get_api().metrics_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.METRICS])
        return response.body

    def list_keys(self, filter: Optional[str] = None, prefix: Optional[str] = None) -> list[str]:
        from .client.paths.keys.get.query_parameters import QueryParametersDict
        from .client.paths.keys_key_prefix.get.path_parameters import (
            PathParametersDict as PrefixPathParametersDict,
        )
        from .client.paths.keys_key_prefix.get.query_parameters import (
            QueryParametersDict as PrefixQueryParametersDict,
        )

        try:
            if prefix is not None:
                prefix_path_params = PrefixPathParametersDict(KeyPrefix=prefix)
                prefix_query_params = None
                if filter is not None:
                    prefix_query_params = PrefixQueryParametersDict(filter=filter)
                response = (
                    self._get_api()
                    .keys_key_prefix_get(
                        path_params=prefix_path_params, query_params=prefix_query_params
                    )
                    .body
                )
            else:
                query_params = None
                if filter is not None:
                    query_params = QueryParametersDict(filter=filter)
                response = self._get_api().keys_get(query_params=query_params).body
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR, Role.OPERATOR])
        return [item.id for item in response]

    def get_key(self, key_id: str) -> Key:
        from .client.paths.keys_key_id.get.path_parameters import PathParametersDict
        from .client.schemas import Unset

        path_params = PathParametersDict(KeyID=key_id)
        try:
            response = self._get_api().keys_key_id_get(path_params=path_params)
            key = response.body
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR, Role.OPERATOR],
                messages={404: f"Key {key_id} not found"},
            )

        mechanisms = [KeyMechanism.from_string(mechanism) for mechanism in key.mechanisms]
        tags = []
        if not isinstance(key.restrictions, Unset):
            if not isinstance(key.restrictions.tags, Unset):
                tags = list(key.restrictions.tags)
        key_type = KeyType.from_string(key.type)

        public_key: PublicKey
        if key_type == KeyType.RSA:
            assert not isinstance(key.public, Unset)
            assert isinstance(key.public.data, Unset)
            assert not isinstance(key.public.modulus, Unset)
            assert not isinstance(key.public.publicExponent, Unset)
            public_key = RsaPublicKey(
                modulus=Base64.from_encoded(key.public.modulus),
                public_exponent=Base64.from_encoded(key.public.publicExponent),
            )
        elif key_type == KeyType.GENERIC:
            if not isinstance(key.public, Unset):
                assert isinstance(key.public.data, Unset)
                assert isinstance(key.public.modulus, Unset)
                assert isinstance(key.public.publicExponent, Unset)
            public_key = None
        else:
            assert not isinstance(key.public, Unset)
            assert not isinstance(key.public.data, Unset)
            assert isinstance(key.public.modulus, Unset)
            assert isinstance(key.public.publicExponent, Unset)
            public_key = EcPublicKey(data=Base64.from_encoded(key.public.data))

        return Key(
            key_id=key_id,
            mechanisms=mechanisms,
            type=key_type,
            operations=key.operations,
            tags=tags,
            public_key=public_key,
        )

    # Get the public key file in PEM format
    def get_key_public_key(self, key_id: str) -> str:
        from .client.paths.keys_key_id_public_pem.get.path_parameters import PathParametersDict

        path_params = PathParametersDict(KeyID=key_id)
        try:
            response = self._get_api().keys_key_id_public_pem_get(
                path_params=path_params, skip_deserialization=True
            )
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR, Role.OPERATOR],
                messages={404: f"Key {key_id} not found"},
            )
        return response.response.data.decode("utf-8")

    def add_key(
        self,
        key_id: Optional[str],
        type: KeyType,
        mechanisms: list[KeyMechanism],
        private_key: PrivateKey,
        tags: list[str] = [],  # noqa: B006
    ) -> str:
        from .client.components.schema.key_mechanisms import KeyMechanismsTupleInput
        from .client.components.schema.key_private_data import KeyPrivateDataDict
        from .client.components.schema.key_restrictions import KeyRestrictionsDict
        from .client.components.schema.private_key import PrivateKeyDict
        from .client.components.schema.tag_list import TagListTuple

        if type == KeyType.RSA:
            assert isinstance(private_key, RsaPrivateKey)
            key_data = KeyPrivateDataDict(
                primeP=private_key.prime_p.data,
                primeQ=private_key.prime_q.data,
                publicExponent=private_key.public_exponent.data,
            )
        else:
            assert isinstance(private_key, GenericPrivateKey)
            key_data = KeyPrivateDataDict(data=private_key.data.data)

        mechanism_tuple: KeyMechanismsTupleInput = [mechanism.value for mechanism in mechanisms]

        if tags:
            body = PrivateKeyDict(
                type=type.value,
                mechanisms=mechanism_tuple,
                private=key_data,
                restrictions=KeyRestrictionsDict(tags=TagListTuple(list(tags))),
            )
        else:
            body = PrivateKeyDict(type=type.value, mechanisms=mechanism_tuple, private=key_data)

        try:
            if key_id:
                from .client.paths.keys_key_id.put.path_parameters import PathParametersDict

                path_params = PathParametersDict(KeyID=key_id)
                self._get_api().keys_key_id_put(path_params=path_params, body=body)
            else:
                response = self._get_api().keys_post(body=body, skip_deserialization=True)
                key_id = self._get_create_resource_id(response.response)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    400: "Bad request -- specified properties are invalid",
                    409: f"Conflict -- a key with the ID {key_id} already exists",
                },
            )
        return key_id

    def add_key_pem(
        self,
        key_id: Optional[str],
        mechanisms: list[KeyMechanism],
        private_key: str,
        tags: list[str] = [],  # noqa: B006
    ) -> str:
        from .client.components.schema.key_mechanisms import KeyMechanismsTupleInput
        from .client.components.schema.key_restrictions import KeyRestrictionsDict
        from .client.components.schema.private_key_pem import ArgumentsDict, PrivateKeyPemDict
        from .client.components.schema.tag_list import TagListTuple

        mechanism_tuple: KeyMechanismsTupleInput = [mechanism.value for mechanism in mechanisms]

        if tags:
            arguments = ArgumentsDict(
                mechanisms=mechanism_tuple,
                restrictions=KeyRestrictionsDict(tags=TagListTuple(list(tags))),
            )
        else:
            arguments = ArgumentsDict(mechanisms=mechanism_tuple)
        body = PrivateKeyPemDict(arguments=arguments, key_file=private_key)

        try:
            if key_id:
                from .client.paths.keys_key_id.put.path_parameters import PathParametersDict

                path_params = PathParametersDict(KeyID=key_id)
                self._get_api().keys_key_id_put(
                    path_params=path_params, body=body, content_type="multipart/form-data"
                )
            else:
                response = self._get_api().keys_post(
                    body=body, content_type="multipart/form-data", skip_deserialization=True
                )
                key_id = self._get_create_resource_id(response.response)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    400: "Bad request -- specified properties are invalid",
                    409: f"Conflict -- a key with the ID {key_id} already exists",
                },
            )
        return key_id

    def delete_key(self, key_id: str) -> None:
        from .client.paths.keys_key_id.delete.path_parameters import PathParametersDict

        path_params = PathParametersDict(KeyID=key_id)
        try:
            self._get_api().keys_key_id_delete(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={404: f"Key {key_id} not found"},
            )

    def move_key(self, old_key_id: str, new_key_id: str) -> None:
        from .client.components.schema.move_key_request import MoveKeyRequestDict
        from .client.paths.keys_key_id_move.post.path_parameters import PathParametersDict

        body = MoveKeyRequestDict(newId=new_key_id)
        path_params = PathParametersDict(KeyID=old_key_id)
        try:
            self._get_api().keys_key_id_move_post(body=body, path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    404: f"Key {old_key_id} not found",
                    409: f"Conflict -- a key with the ID {new_key_id} already exists",
                },
            )

    def generate_key(
        self,
        type: KeyType,
        mechanisms: list[KeyMechanism],
        length: int,
        key_id: Optional[str] = None,
    ) -> str:
        from .client.components.schema.key_generate_request_data import KeyGenerateRequestDataDict
        from .client.components.schema.key_mechanisms import KeyMechanismsTupleInput

        mechanism_tuple: KeyMechanismsTupleInput = [mechanism.value for mechanism in mechanisms]

        if key_id:
            body = KeyGenerateRequestDataDict(
                type=type.value, mechanisms=mechanism_tuple, length=length, id=key_id
            )
        else:
            body = KeyGenerateRequestDataDict(
                type=type.value, mechanisms=mechanism_tuple, length=length
            )
        try:
            response = self._get_api().keys_generate_post(body=body, skip_deserialization=True)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    400: "Bad request -- invalid input data",
                    409: f"Conflict -- a key with the ID {key_id} already exists",
                },
            )
        return self._get_create_resource_id(response.response)

    def get_config_logging(self) -> LoggingConfig:
        try:
            response = self._get_api().config_logging_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return LoggingConfig(
            ip_address=response.body.ipAddress,
            log_level=LogLevel.from_string(response.body.logLevel),
            port=response.body.port,
        )

    def get_config_network(self) -> NetworkConfig:
        try:
            response = self._get_api().config_network_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return NetworkConfig._from_api(response.body)

    def get_config_time(self) -> datetime:
        try:
            response = self._get_api().config_time_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        # could be replaced with datetime.fromisoformat in Python 3.11 or later
        return datetime.strptime(response.body.time, "%Y-%m-%dT%H:%M:%SZ").replace(
            tzinfo=timezone.utc
        )

    def get_config_unattended_boot(self) -> str:
        try:
            response = self._get_api().config_unattended_boot_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return response.body.status

    def get_public_key(self) -> str:
        try:
            response = self._get_api().config_tls_public_pem_get(skip_deserialization=True)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return response.response.data.decode("utf-8")

    def get_certificate(self) -> str:
        try:
            response = self._get_api().config_tls_cert_pem_get(skip_deserialization=True)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return response.response.data.decode("utf-8")

    def get_key_certificate(self, key_id: str) -> bytes:
        try:
            from .client.paths.keys_key_id_cert.get.path_parameters import PathParametersDict

            path_params = PathParametersDict(KeyID=key_id)

            response = self._get_api().keys_key_id_cert_get(
                path_params=path_params, skip_deserialization=True
            )
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR, Role.OPERATOR],
                messages={
                    404: f"Certificate for key {key_id} not found",
                    # The API returns a 406 if there is no certificate or if the key does not exist
                    406: f"Certificate for key {key_id} not found",
                },
            )
        return response.response.data

    def set_certificate(self, cert: Bytes) -> None:
        try:
            self._request(
                "PUT", "config/tls/cert.pem", data=cert, mime_type="application/x-pem-file"
            )
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={400: "Bad Request -- invalid certificate"},
            )

    def set_key_certificate(self, key_id: str, cert: Bytes) -> None:
        try:
            from .client.paths.keys_key_id_cert.put.path_parameters import PathParametersDict

            path_params = PathParametersDict(KeyID=key_id)

            self._get_api().keys_key_id_cert_put(body=cert, path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    400: "Bad Request -- invalid certificate",
                    404: f"Key {key_id} not found",
                    409: f"Conflict -- key {key_id} already has a certificate",
                    415: "Invalid mime type",
                },
            )

    def delete_key_certificate(self, key_id: str) -> None:
        from .client.paths.keys_key_id_cert.delete.path_parameters import PathParametersDict

        path_params = PathParametersDict(KeyID=key_id)
        try:
            self._get_api().keys_key_id_cert_delete(path_params=path_params)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={404: f"There is no certificate for {key_id}."},
            )

    def csr(
        self,
        *,
        common_name: str,
        country: Optional[str] = None,
        state_or_province: Optional[str] = None,
        locality: Optional[str] = None,
        organization: Optional[str] = None,
        organizational_unit: Optional[str] = None,
        email_address: Optional[str] = None,
        subject_alt_names: Optional[list[str]] = None,
    ) -> str:
        from .client.components.schema.distinguished_name import DistinguishedNameDict

        body = DistinguishedNameDict(
            commonName=common_name,
            countryName=_optional_to_unset(country),
            stateOrProvinceName=_optional_to_unset(state_or_province),
            localityName=_optional_to_unset(locality),
            organizationName=_optional_to_unset(organization),
            organizationalUnitName=_optional_to_unset(organizational_unit),
            emailAddress=_optional_to_unset(email_address),
            subjectAltNames=_optional_to_unset(subject_alt_names),
        )
        try:
            response = self._get_api().config_tls_csr_pem_post(body=body, skip_deserialization=True)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return response.response.data.decode("utf-8")

    def generate_tls_key(self, type: TlsKeyType, length: Optional[int] = None) -> None:
        from .client.components.schema.tls_key_generate_request_data import (
            TlsKeyGenerateRequestDataDict,
        )

        body = TlsKeyGenerateRequestDataDict(type=type.value, length=_optional_to_unset(length))

        try:
            self._get_api().config_tls_generate_post(body=body)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def key_csr(
        self,
        key_id: str,
        *,
        common_name: str,
        country: Optional[str] = None,
        state_or_province: Optional[str] = None,
        locality: Optional[str] = None,
        organization: Optional[str] = None,
        organizational_unit: Optional[str] = None,
        email_address: Optional[str] = None,
        subject_alt_names: Optional[list[str]] = None,
    ) -> str:
        from .client.components.schema.distinguished_name import DistinguishedNameDict
        from .client.paths.keys_key_id_csr_pem.post.path_parameters import PathParametersDict

        path_params = PathParametersDict(KeyID=key_id)
        body = DistinguishedNameDict(
            commonName=common_name,
            countryName=_optional_to_unset(country),
            stateOrProvinceName=_optional_to_unset(state_or_province),
            localityName=_optional_to_unset(locality),
            organizationName=_optional_to_unset(organization),
            organizationalUnitName=_optional_to_unset(organizational_unit),
            emailAddress=_optional_to_unset(email_address),
            subjectAltNames=_optional_to_unset(subject_alt_names),
        )
        try:
            response = self._get_api().keys_key_id_csr_pem_post(
                path_params=path_params, body=body, skip_deserialization=True
            )
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR, Role.OPERATOR],
                messages={404: f"Key {key_id} not found"},
            )
        return response.response.data.decode("utf-8")

    def set_backup_passphrase(
        self, new_passphrase: str, current_passphrase: Optional[str] = None
    ) -> None:
        from .client.components.schema.backup_passphrase_config import BackupPassphraseConfigDict

        body = BackupPassphraseConfigDict(
            newPassphrase=new_passphrase, currentPassphrase=current_passphrase or ""
        )
        try:
            self._get_api().config_backup_passphrase_put(body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={400: "Bad request -- e. g. weak passphrase"},
            )

    def set_unlock_passphrase(self, new_passphrase: str, current_passphrase: str) -> None:
        from .client.components.schema.unlock_passphrase_config import UnlockPassphraseConfigDict

        body = UnlockPassphraseConfigDict(
            newPassphrase=new_passphrase, currentPassphrase=current_passphrase
        )
        try:
            self._get_api().config_unlock_passphrase_put(body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={400: "Bad request -- e. g. weak passphrase"},
            )

    def set_logging_config(self, ip_address: str, port: int, log_level: LogLevel) -> None:
        from .client.components.schema.logging_config import LoggingConfigDict

        body = LoggingConfigDict(ipAddress=ip_address, port=port, logLevel=log_level.value)
        try:
            self._get_api().config_logging_put(body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={400: "Bad request -- invalid input data"},
            )

    def set_network_config(
        self, ip_address: str, netmask: str, gateway: str, ipv6: Optional[Ipv6Config] = None
    ) -> None:
        from .client.components.schema.network_config_input import NetworkConfigInputDict

        ipv6_dict = ipv6._to_api() if ipv6 is not None else None
        body = NetworkConfigInputDict(
            ipAddress=ip_address,
            netmask=netmask,
            gateway=gateway,
            ipv6=_optional_to_unset(ipv6_dict),
        )
        try:
            self._get_api().config_network_put(body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={400: "Bad request -- invalid input data"},
            )

    def set_time(self, time: Union[str, datetime]) -> None:
        from .client.components.schema.time_config import TimeConfigDict

        body = TimeConfigDict(time=time)
        try:
            self._get_api().config_time_put(body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={400: "Bad request -- invalid time format"},
            )

    def set_unattended_boot(self, status: UnattendedBootStatus) -> None:
        from .client.components.schema.unattended_boot_config import UnattendedBootConfigDict

        body = UnattendedBootConfigDict(status=status.value)
        try:
            self._get_api().config_unattended_boot_put(body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={400: "Bad request -- invalid status setting"},
            )

    def get_system_info(self) -> SystemInfo:
        try:
            response = self._get_api().system_info_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return SystemInfo(
            firmware_version=response.body.firmwareVersion,
            software_version=response.body.softwareVersion,
            hardware_version=response.body.hardwareVersion,
            build_tag=response.body.softwareBuild,
            tpm=Tpm(
                attestation_keys=dict(response.body.akPub),
                platform_configuration_registers=dict(response.body.pcr),
            ),
        )

    def backup(self) -> bytes:
        try:
            response = self._get_api().system_backup_post()
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.BACKUP],
                messages={412: "NetHSM is not Operational or the backup passphrase is not set"},
            )
        return response.response.data

    def restore(self, backup: Bytes, passphrase: str, time: Optional[datetime]) -> None:
        try:
            from .client.components.schema.restore_request import ArgumentsDict, RestoreRequestDict

            if not time:
                time = datetime.now(timezone.utc)

            body = RestoreRequestDict(
                arguments=ArgumentsDict(backupPassphrase=passphrase, systemTime=time),
                backup_file=backup,
            )

            if self.auth:
                self._get_api().system_restore_post(body, security_index=1)
            else:
                self._get_api().system_restore_post(body)
        except Exception as e:
            _handle_exception(
                e, state=State.UNPROVISIONED, messages={400: "Bad request -- backup did not apply"}
            )

    def update(self, image: Bytes) -> str:
        try:
            response = self._get_api().system_update_post(body=image)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.ADMINISTRATOR],
                messages={
                    400: "Bad request -- malformed image",
                    409: "Conflict -- major version downgrade is not allowed",
                },
            )

        # Manually read the release notes from the response body
        json_str = response.response.data.decode("utf-8")
        release_notes = json.loads(json_str)["releaseNotes"]
        assert isinstance(release_notes, str)
        return release_notes

        # # Use this one when the bug is fixed:
        # return response.body.releaseNotes

    def cancel_update(self) -> None:
        try:
            self._get_api().system_cancel_update_post()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def commit_update(self) -> None:
        try:
            self._get_api().system_commit_update_post()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def reboot(self) -> None:
        try:
            self._get_api().system_reboot_post()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def shutdown(self) -> None:
        try:
            if self.auth:
                self._get_api().system_shutdown_post()
            else:
                self._get_api().system_shutdown_post(security_index=1)
        except Exception as e:
            _handle_exception(e)

    def factory_reset(self) -> None:
        try:
            self._get_api().system_factory_reset_post()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def encrypt(
        self, key_id: str, data: Base64, mode: EncryptMode, iv: Optional[Base64] = None
    ) -> EncryptionResult:
        from .client.components.schema.encrypt_request_data import EncryptRequestDataDict
        from .client.paths.keys_key_id_encrypt.post.path_parameters import PathParametersDict
        from .client.schemas import Unset

        path_params = PathParametersDict(KeyID=key_id)
        body = EncryptRequestDataDict(
            message=data.data, mode=mode.value, iv=iv.data if iv else Unset()
        )
        try:
            response = self._get_api().keys_key_id_encrypt_post(path_params=path_params, body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.OPERATOR],
                messages={
                    400: "Bad request -- e. g. invalid encryption mode, or wrong padding",
                    404: f"Key {key_id} not found",
                },
            )
        return EncryptionResult(
            encrypted=Base64.from_encoded(response.body.encrypted),
            iv=Base64.from_encoded(response.body.iv),
        )

    def decrypt(
        self, key_id: str, data: Base64, mode: DecryptMode, iv: Optional[Base64] = None
    ) -> Base64:
        from .client.components.schema.decrypt_request_data import DecryptRequestDataDict
        from .client.paths.keys_key_id_decrypt.post.path_parameters import PathParametersDict
        from .client.schemas import Unset

        body = DecryptRequestDataDict(
            encrypted=data.data, mode=mode.value, iv=iv.data if iv else Unset()
        )

        path_params = PathParametersDict(KeyID=key_id)
        try:
            response = self._get_api().keys_key_id_decrypt_post(path_params=path_params, body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.OPERATOR],
                messages={
                    400: "Bad request -- e. g. invalid encryption mode",
                    404: f"Key {key_id} not found",
                },
            )
        return Base64.from_encoded(response.body.decrypted)

    def sign(self, key_id: str, data: Base64, mode: SignMode) -> Base64:
        from .client.components.schema.sign_request_data import SignRequestDataDict
        from .client.paths.keys_key_id_sign.post.path_parameters import PathParametersDict

        path_params = PathParametersDict(KeyID=key_id)
        body = SignRequestDataDict(message=data.data, mode=mode.value)
        try:
            response = self._get_api().keys_key_id_sign_post(path_params=path_params, body=body)
        except Exception as e:
            _handle_exception(
                e,
                state=State.OPERATIONAL,
                roles=[Role.OPERATOR],
                messages={
                    400: "Bad request -- e. g. invalid sign mode",
                    404: f"Key {key_id} not found",
                },
            )
        return Base64.from_encoded(response.body.signature)

    def list_cluster_members(self) -> list[ClusterMember]:
        try:
            response = self._get_api().cluster_members_get()
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

        return [ClusterMember._from_api(item) for item in response.body]

    def add_cluster_member(self, urls: list[str]) -> ClusterJoinData:
        from .client.components.schema.cluster_add_req import ClusterAddReqDict

        body = ClusterAddReqDict(urls=urls)
        try:
            response = self._get_api().cluster_members_post(body=body)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

        return ClusterJoinData._from_api(response.body)

    def set_cluster_member_urls(self, member_id: str, urls: list[str]) -> None:
        from .client.components.schema.cluster_add_req import ClusterAddReqDict
        from .client.paths.cluster_members_member_id.put.path_parameters import PathParametersDict

        body = ClusterAddReqDict(urls=urls)
        path_params = PathParametersDict(MemberID=member_id)
        try:
            self._get_api().cluster_members_member_id_put(body=body, path_params=path_params)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def remove_cluster_member(self, member_id: str) -> None:
        from .client.paths.cluster_members_member_id.delete.path_parameters import (
            PathParametersDict,
        )

        path_params = PathParametersDict(MemberID=member_id)
        try:
            self._get_api().cluster_members_member_id_delete(path_params=path_params)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def join_cluster(self, data: ClusterJoinData, backup_passphrase: str) -> None:
        from .client.components.schema.cluster_initial_member import ClusterInitialMemberDict
        from .client.components.schema.cluster_join_req import ClusterJoinReqDict, MembersTupleInput

        members: MembersTupleInput = [
            ClusterInitialMemberDict(name=member.name, urls=member.urls) for member in data.members
        ]
        body = ClusterJoinReqDict(
            members=members, joinerKit=data.joiner_kit, backupPassphrase=backup_passphrase
        )
        try:
            self._get_api().cluster_join_post(body=body)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])

    def get_cluster_ca_certificate(self) -> str:
        try:
            response = self._get_api().config_tls_cluster_ca_pem_get(skip_deserialization=True)
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])
        return response.response.data.decode("utf-8")

    def set_cluster_ca_certificate(self, cert: Bytes) -> None:
        try:
            self._request("PUT", "config/tls/ca.pem", data=cert, mime_type="application/x-pem-file")
        except Exception as e:
            _handle_exception(e, state=State.OPERATIONAL, roles=[Role.ADMINISTRATOR])


@contextlib.contextmanager
def connect(
    host: str, auth: Optional[Authentication] = None, verify_tls: bool = True
) -> Iterator[NetHSM]:
    nethsm = NetHSM(host, auth, verify_tls)
    try:
        yield nethsm
    finally:
        nethsm.close()
