"""
MLS group state manager: rfc9420 integration for DAVE.

Creates group, key package, processes commit/welcome, exports sender base secret.
Used by DaveSession for group lifecycle; can also be used for custom flows.

Public functions:
    get_dave_crypto_provider: DAVE MLS ciphersuite crypto provider.
    create_key_package: Key package for a user; used by DaveSession.prepare_epoch(1).
    create_group: New MLS group with one member; used when handling opcode 25.
    join_from_welcome: Join from Welcome message; used by DaveSession.handle_welcome.
    export_sender_base_secret: 16-byte base secret for KeyRatchet; used when refreshing ratchets.
    apply_commit: Apply received commit; used by DaveSession.handle_commit.
    process_proposal: Apply one proposal; used by DaveSession.handle_proposals.
    create_commit_and_welcome: Build commit and welcomes; used by DaveSession.handle_proposals.
    create_remove_proposal_for_self: Remove proposal for local member; used by DaveSession.leave_group.
    create_update_proposal: Update proposal to refresh leaf keys; optional.
    validate_group_dave_ciphersuite_and_extensions: Check ciphersuite and extension list for DAVE.
    validate_group_external_sender: Check external sender matches; used when applying commits.
    get_external_senders_from_group: Read external senders from group context.
"""

from __future__ import annotations

import contextlib
import struct
from typing import Union

from rfc9420 import DefaultCryptoProvider
from rfc9420.api.session import MLSGroupSession

from sorrydave.exceptions import InvalidCommitError

# DAVE protocol v1: MLS ciphersuite 2 (DHKEMP256_AES128GCM_SHA256_P256)
DAVE_MLS_CIPHERSUITE_ID = 2
EXPORTER_LABEL = b"Discord Secure Frames v0"
EXPORTER_LENGTH = 16
EXTENSION_TYPE_EXTERNAL_SENDERS = 0x0002


def _read_varint(data: bytes, offset: int) -> tuple[int, int]:
    """Read MLS-style varint from data at offset. Returns (value, new_offset)."""
    if offset >= len(data):
        raise ValueError("Varint truncated")
    first = data[offset]
    prefix = first >> 6
    if prefix == 0b00:
        return first & 0x3F, offset + 1
    if prefix == 0b01:
        if offset + 2 > len(data):
            raise ValueError("Varint truncated")
        value = ((first & 0x3F) << 8) | data[offset + 1]
        return value, offset + 2
    if prefix == 0b10:
        if offset + 4 > len(data):
            raise ValueError("Varint truncated")
        value = (
            ((first & 0x3F) << 24)
            | (data[offset + 1] << 16)
            | (data[offset + 2] << 8)
            | data[offset + 3]
        )
        return value, offset + 4
    raise ValueError("Varint overflow")


def _read_opaque_varint(data: bytes, offset: int) -> tuple[bytes, int]:
    """Read opaque<V>: varint length then that many bytes."""
    length, pos = _read_varint(data, offset)
    if pos + length > len(data):
        raise ValueError("Opaque truncated")
    return data[pos : pos + length], pos + length


def get_dave_crypto_provider() -> DefaultCryptoProvider:
    """
    Return rfc9420 DefaultCryptoProvider with DAVE MLS ciphersuite (2).

    Used by DaveSession when creating key packages and groups.

    Returns:
        DefaultCryptoProvider: Instance for DHKEMP256_AES128GCM_SHA256_P256.
    """
    return DefaultCryptoProvider(suite_id=DAVE_MLS_CIPHERSUITE_ID)


def create_key_package(
    user_id: int,
    crypto: Union[DefaultCryptoProvider, None] = None,
) -> tuple[bytes, bytes, bytes]:
    """
    Create a KeyPackage for the given user_id (64-bit e.g. Discord snowflake).

    Used by DaveSession when prepare_epoch(1) is called. Identity is big-endian user_id (8 bytes).
    Lifetime not_before=0, not_after=2^64-1.

    Args:
        user_id (int): User identifier (64-bit).
        crypto (Union[DefaultCryptoProvider, None]): Crypto provider; uses get_dave_crypto_provider() if None.

    Returns:
        tuple[bytes, bytes, bytes]: (key_package_serialized, hpke_private_key, signing_key_der).

    Raises:
        ValueError: If MLS ciphersuite is unknown.
    """
    if crypto is None:
        crypto = get_dave_crypto_provider()

    from rfc9420.crypto.ciphersuites import get_ciphersuite_by_id
    from rfc9420.messages.data_structures import (
        CipherSuite,
        Credential,
        CredentialType,
        MLSVersion,
        Signature,
    )
    from rfc9420.messages.key_packages import KeyPackage, LeafNode, LeafNodeSource

    cs = get_ciphersuite_by_id(DAVE_MLS_CIPHERSUITE_ID)
    if cs is None:
        raise ValueError(f"Unknown MLS ciphersuite id {DAVE_MLS_CIPHERSUITE_ID}")
    cipher_suite = CipherSuite(cs.kem, cs.kdf, cs.aead, suite_id=cs.suite_id)
    # Generate HPKE key pair (for init_key and leaf encryption_key)
    hpke_private, hpke_public = crypto.generate_key_pair()
    # Generate ECDSA P256 signing key pair for leaf
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.primitives.asymmetric import ec

    sig_private = ec.generate_private_key(ec.SECP256R1())
    sig_private_der = sig_private.private_bytes(
        serialization.Encoding.DER,
        serialization.PrivateFormat.PKCS8,
        serialization.NoEncryption(),
    )
    sig_public_bytes = sig_private.public_key().public_bytes(
        serialization.Encoding.X962,
        serialization.PublicFormat.UncompressedPoint,
    )
    if sig_public_bytes[0] != 0x04:
        sig_public_bytes = b"\x04" + sig_public_bytes
    identity = user_id.to_bytes(8, "big")
    credential = Credential(
        identity=identity,
        public_key=sig_public_bytes,
        credential_type=CredentialType.BASIC,
    )
    from rfc9420.extensions.extensions import build_capabilities_data

    capabilities = build_capabilities_data(
        ciphersuite_ids=[DAVE_MLS_CIPHERSUITE_ID],
        supported_exts=[],
        include_grease=False,
    )
    lifetime_not_before = 0
    lifetime_not_after = (1 << 64) - 1
    leaf_node = LeafNode(
        encryption_key=hpke_public,
        signature_key=sig_public_bytes,
        credential=credential,
        capabilities=capabilities,
        leaf_node_source=LeafNodeSource.KEY_PACKAGE,
        lifetime_not_before=lifetime_not_before,
        lifetime_not_after=lifetime_not_after,
        parent_hash=b"",
        extensions=[],
        signature=Signature(b""),
    )
    leaf_tbs = leaf_node.tbs_serialize()
    leaf_sig = crypto.sign(sig_private_der, leaf_tbs)
    leaf_node = LeafNode(
        encryption_key=hpke_public,
        signature_key=sig_public_bytes,
        credential=credential,
        capabilities=capabilities,
        leaf_node_source=LeafNodeSource.KEY_PACKAGE,
        lifetime_not_before=lifetime_not_before,
        lifetime_not_after=lifetime_not_after,
        parent_hash=b"",
        extensions=[],
        signature=Signature(leaf_sig),
    )
    init_key = hpke_public  # DAVE uses same as leaf encryption key per typical MLS
    kp = KeyPackage(
        version=MLSVersion.MLS10,
        cipher_suite=cipher_suite,
        init_key=init_key,
        leaf_node=leaf_node,
        extensions=[],
        signature=Signature(b""),
    )
    kp_tbs = kp.tbs_serialize()
    kp_sig = crypto.sign(sig_private_der, kp_tbs)
    kp = KeyPackage(
        version=MLSVersion.MLS10,
        cipher_suite=cipher_suite,
        init_key=init_key,
        leaf_node=leaf_node,
        extensions=[],
        signature=Signature(kp_sig),
    )
    return (kp.serialize(), hpke_private, sig_private_der)


def _write_varint(x: int) -> bytes:
    """RFC 9420 variable-length integer encoding."""
    if x < 0x40:
        return bytes([x])
    if x < 0x4000:
        return (x | 0x4000).to_bytes(2, "big")
    if x <= 0x3FFFFFFF:
        return (x | 0x80000000).to_bytes(4, "big")
    raise ValueError("integer too large for RFC 9420 varint")


def _write_opaque_varint(data: bytes) -> bytes:
    """Encode opaque<V>: varint length prefix + data."""
    return _write_varint(len(data)) + data


def serialize_external_senders_extension(
    signature_key: bytes,
    credential_type: int,
    identity: bytes,
) -> bytes:
    """Serialize the external_senders group extension for the MLS GroupContext.
    Returns the raw bytes for the GroupContext.extensions field (inner content
    of Extension extensions<V>, without the outer vector length prefix).
    Wire format per RFC 9420 section 12.1.8.1:
        Extension { uint16 extension_type; opaque extension_data<V>; }
        ExternalSendersExtension { ExternalSender external_senders<V>; }
        ExternalSender { SignaturePublicKey signature_key; Credential credential; }
        Credential { uint16 credential_type; opaque identity<V>; }
    """
    ext_sender = _write_opaque_varint(signature_key)
    ext_sender += struct.pack("!H", credential_type)
    ext_sender += _write_opaque_varint(identity)
    ext_senders_list = _write_opaque_varint(ext_sender)
    extension = struct.pack("!H", EXTENSION_TYPE_EXTERNAL_SENDERS)
    extension += _write_opaque_varint(ext_senders_list)
    return extension


def get_external_senders_from_group(session: MLSGroupSession) -> list[tuple[bytes, int, bytes]]:
    """
    Parse GroupContext.extensions and return list of (signature_key, credential_type, identity)
    for the external_senders extension (type 0x0002).

    Note: Uses ``session._group._inner._group_context`` because the rfc9420 public API
    does not expose group context extensions. Required for protocol-mandated external
    sender validation (protocol.md §Invalid Groups).

    Returns:
        list[tuple[bytes, int, bytes]]: Empty if no external senders extension or parse error.
    """
    try:
        inner = session._group._inner
        gc = inner._group_context
        if gc is None or not gc.extensions:
            return []
    except Exception:
        return []
    data = gc.extensions if isinstance(gc.extensions, bytes) else b""
    if not data:
        return []
    try:
        n_ext, pos = _read_varint(data, 0)
        result: list[tuple[bytes, int, bytes]] = []
        for _ in range(n_ext):
            if pos + 2 > len(data):
                break
            ext_type = struct.unpack("!H", data[pos : pos + 2])[0]
            pos += 2
            ext_data, pos = _read_opaque_varint(data, pos)
            if ext_type != EXTENSION_TYPE_EXTERNAL_SENDERS:
                continue
            ext_senders_list, off = _read_opaque_varint(ext_data, 0)
            if off != len(ext_data):
                continue
            while off < len(ext_senders_list):
                sig_key, off = _read_opaque_varint(ext_senders_list, off)
                if off + 2 > len(ext_senders_list):
                    break
                cred_type = struct.unpack("!H", ext_senders_list[off : off + 2])[0]
                off += 2
                identity, off = _read_opaque_varint(ext_senders_list, off)
                result.append((sig_key, cred_type, identity))
        return result
    except Exception:
        return []


def validate_group_dave_ciphersuite_and_extensions(session: MLSGroupSession) -> None:
    """
    Verify the group has the expected DAVE ciphersuite and extension list (protocol version).

    Per protocol: group must have expected ciphersuite and extension list (external_senders only).

    Raises:
        InvalidCommitError: If ciphersuite is not DAVE or extensions are not exactly external_senders.
    """
    try:
        inner = session._group._inner
        gc = inner._group_context
        if gc is None:
            raise InvalidCommitError("Group has no group context")
        if getattr(gc, "cipher_suite_id", None) != DAVE_MLS_CIPHERSUITE_ID:
            raise InvalidCommitError(
                f"Group ciphersuite must be {DAVE_MLS_CIPHERSUITE_ID}; got {getattr(gc, 'cipher_suite_id', None)}"
            )
        data = gc.extensions if isinstance(gc.extensions, bytes) else b""
        if not data:
            raise InvalidCommitError(
                "Group must have exactly one extension (external_senders)"
            )
        n_ext, pos = _read_varint(data, 0)
        if n_ext != 1:
            raise InvalidCommitError(
                f"Group must have exactly one extension (external_senders); got {n_ext}"
            )
        if pos + 2 > len(data):
            raise InvalidCommitError("Group extensions truncated")
        ext_type = struct.unpack("!H", data[pos : pos + 2])[0]
        if ext_type != EXTENSION_TYPE_EXTERNAL_SENDERS:
            raise InvalidCommitError(
                f"Group extension must be external_senders (0x0002); got 0x{ext_type:04x}"
            )
    except InvalidCommitError:
        raise
    except Exception as e:
        raise InvalidCommitError("Invalid group ciphersuite or extensions") from e


def validate_group_external_sender(
    session: MLSGroupSession,
    expected_signature_key: bytes,
    expected_credential_type: int,
    expected_identity: bytes,
) -> None:
    """
    Verify the group has exactly one external sender matching the voice gateway package.

    Raises:
        InvalidCommitError: If not exactly one external sender or no match.
    """
    senders = get_external_senders_from_group(session)
    if len(senders) != 1:
        raise InvalidCommitError(
            f"Group must have exactly one external sender; got {len(senders)}"
        )
    sig_key, cred_type, identity = senders[0]
    if (
        sig_key != expected_signature_key
        or cred_type != expected_credential_type
        or identity != expected_identity
    ):
        raise InvalidCommitError(
            "Group external sender does not match voice gateway package"
        )


def create_group(
    group_id: bytes,
    key_package_bytes: bytes,
    crypto: Union[DefaultCryptoProvider, None] = None,
    external_sender_signature_key: Union[bytes, None] = None,
    external_sender_credential_type: int = 1,
    external_sender_identity: Union[bytes, None] = None,
) -> MLSGroupSession:
    """
    Create a new MLS group with the given key package (single member).

    Used by DaveSession when handle_external_sender_package is called and a key package
    was already prepared. When external sender parameters are provided, the group
    extensions include the required external_senders extension (type 0x0002) per
    RFC 9420 section 12.1.8.1. This is mandatory for the DAVE protocol.
    """
    from rfc9420 import GroupConfig, MemoryStorageProvider
    from rfc9420.api.session import MLSGroupSession
    from rfc9420.messages.key_packages import KeyPackage

    if crypto is None:
        crypto = get_dave_crypto_provider()
    storage = MemoryStorageProvider()
    config = GroupConfig(crypto_provider=crypto, storage_provider=storage)

    initial_extensions = b""
    if (
        external_sender_signature_key is not None
        and external_sender_identity is not None
    ):
        initial_extensions = serialize_external_senders_extension(
            signature_key=external_sender_signature_key,
            credential_type=external_sender_credential_type,
            identity=external_sender_identity,
        )

    kp = KeyPackage.deserialize(key_package_bytes)
    return MLSGroupSession.create_with_config(
        config, group_id, kp, initial_extensions=initial_extensions
    )


def join_from_welcome(
    welcome_bytes: bytes,
    hpke_private_key: bytes,
    crypto: Union[DefaultCryptoProvider, None] = None,
) -> MLSGroupSession:
    """
    Join group from Welcome message.

    Used by DaveSession when handle_welcome is called (client was added to the group).

    Args:
        welcome_bytes (bytes): Serialized MLS Welcome message.
        hpke_private_key (bytes): HPKE private key from the KeyPackage used in the Add.
        crypto (Union[DefaultCryptoProvider, None]): Crypto provider; uses get_dave_crypto_provider() if None.

    Returns:
        MLSGroupSession: Session for the joined group.
    """
    from rfc9420 import GroupConfig, MemoryStorageProvider
    from rfc9420.api.session import MLSGroupSession
    from rfc9420.messages.data_structures import Welcome

    if crypto is None:
        crypto = get_dave_crypto_provider()
    storage = MemoryStorageProvider()
    config = GroupConfig(crypto_provider=crypto, storage_provider=storage)
    welcome = Welcome.deserialize(welcome_bytes)
    return MLSGroupSession.join_from_welcome_with_config(
        config, welcome, hpke_private_key
    )


def export_sender_base_secret(session: MLSGroupSession, sender_user_id: int) -> bytes:
    """
    Export 16-byte sender base secret via MLS Exporter.

    Used by DaveSession when refreshing send/receive ratchets (KeyRatchet base secret).
    Uses label "Discord Secure Frames v0" and context = little-endian 64-bit sender user ID.

    Args:
        session (MLSGroupSession): MLS group session.
        sender_user_id (int): Sender user ID (64-bit).

    Returns:
        bytes: 16-byte base secret for KeyRatchet.
    """
    context = sender_user_id.to_bytes(8, "little")
    result: bytes = session.export_secret(EXPORTER_LABEL, context, EXPORTER_LENGTH)
    return result


def iter_members(session: MLSGroupSession) -> list[tuple[int, bytes]]:
    """
    Return (leaf_index, identity) for each member using the session's ratchet tree.

    Identity is the credential identity bytes (e.g. big-endian user ID); empty bytes if none.
    """
    result: list[tuple[int, bytes]] = []
    with contextlib.suppress(Exception):
        inner = session._group._inner
        tree = inner._ratchet_tree
        for leaf in range(tree.n_leaves):
            node = tree.get_node(leaf * 2)
            identity = b""
            if node.leaf_node and node.leaf_node.credential is not None:
                identity = getattr(node.leaf_node.credential, "identity", b"") or b""
            result.append((leaf, identity))
    return result


def _iter_members(session: MLSGroupSession) -> list[tuple[int, bytes]]:
    """Alias for iter_members used internally."""
    return iter_members(session)


def _check_no_duplicate_credentials(session: MLSGroupSession) -> None:
    """
    Raise InvalidCommitError if the group tree has duplicate basic credentials (user IDs).

    Per DAVE client commit validity: "The resulting group includes a duplicated basic
    credential (i.e. the big-endian user ID snowflake) between two or more leaf nodes."
    """
    seen: set[bytes] = set()
    try:
        for _leaf_index, identity in _iter_members(session):
            if not identity or len(identity) < 8:
                continue
            id_bytes = identity[:8]
            if id_bytes in seen:
                raise InvalidCommitError("Duplicate basic credential in group tree")
            seen.add(id_bytes)
    except InvalidCommitError:
        raise
    except Exception:
        return


def apply_commit(
    session: MLSGroupSession,
    commit_mls_plaintext_bytes: bytes,
    sender_leaf_index: int,
) -> None:
    """
    Apply a received commit to the group.

    Used by DaveSession when handle_commit is called. Per DAVE client commit validity,
    raises InvalidCommitError if the resulting group would have duplicate basic
    credentials (user IDs).

    Args:
        session (MLSGroupSession): MLS group session.
        commit_mls_plaintext_bytes (bytes): Serialized MLS Plaintext commit message.
        sender_leaf_index (int): Leaf index of the commit sender.

    Raises:
        InvalidCommitError: If commit application fails or duplicate credentials.
    """
    try:
        session.apply_commit(commit_mls_plaintext_bytes, sender_leaf_index)
        _check_no_duplicate_credentials(session)
    except InvalidCommitError:
        raise
    except Exception as e:
        raise InvalidCommitError("Failed to apply commit") from e


def process_proposal(
    session: MLSGroupSession,
    proposal_mls_plaintext_bytes: bytes,
    sender_leaf_index: int,
    sender_type: int = 1,
) -> None:
    """
    Process a proposal (e.g. Add/Remove from external sender).

    Used by DaveSession when handle_proposals is called to apply each proposal
    before creating a commit.

    Args:
        session (MLSGroupSession): MLS group session.
        proposal_mls_plaintext_bytes (bytes): Serialized MLS Plaintext proposal.
        sender_leaf_index (int): Leaf index of the sender.
        sender_type (int): 1 = MEMBER, 2 = EXTERNAL. Defaults to 1.
    """
    from rfc9420.interop.wire import decode_handshake

    msg = decode_handshake(proposal_mls_plaintext_bytes)
    session._group.process_proposal(msg, sender_leaf_index, sender_type=sender_type)


def create_commit_and_welcome(
    session: MLSGroupSession, signing_key_der: bytes
) -> tuple[bytes, list[bytes]]:
    """
    Create commit and optional welcome messages.

    Used by DaveSession when handle_proposals returns the opcode 28 payload (commit + optional welcome).

    Args:
        session (MLSGroupSession): MLS group session with pending proposals.
        signing_key_der (bytes): Signing private key (DER) for the committing member.

    Returns:
        tuple[bytes, list[bytes]]: (serialized commit plaintext, list of serialized Welcome bytes).
    """
    commit_bytes, welcomes = session.commit(
        signing_key_der, return_per_joiner_welcomes=True
    )
    welcome_list: list[bytes] = [w.serialize() for w in welcomes]
    return (commit_bytes, welcome_list)


def create_remove_proposal_for_self(
    session: MLSGroupSession, signing_key_der: bytes
) -> bytes:
    """
    Create an MLS Remove proposal for the local member (self-remove).

    Used by DaveSession when leave_group is called; return value is sent as opcode 27.

    Args:
        session (MLSGroupSession): MLS group session.
        signing_key_der (bytes): Signing private key (DER) for the member.

    Returns:
        bytes: Serialized MLS Plaintext proposal to send (e.g. via opcode 27).
    """
    return session.remove_member(session.own_leaf_index, signing_key_der)


def create_update_proposal(
    session: MLSGroupSession,
    signing_key_der: bytes,
    user_id: int,
    crypto: Union[DefaultCryptoProvider, None] = None,
) -> bytes:
    """
    Create an MLS Update proposal to refresh the local member's leaf node keys.

    Args:
        session (MLSGroupSession): MLS group session.
        signing_key_der (bytes): Signing private key (DER) for the member.
        user_id (int): User ID for credential (64-bit).
        crypto (Union[DefaultCryptoProvider, None]): Crypto provider; uses get_dave_crypto_provider() if None.

    Returns:
        bytes: Serialized MLS Plaintext proposal to send (e.g. via opcode 27).

    Raises:
        ValueError: If MLS ciphersuite is unknown.
    """
    if crypto is None:
        crypto = get_dave_crypto_provider()
    from rfc9420.crypto.ciphersuites import get_ciphersuite_by_id
    from rfc9420.messages.data_structures import (
        Credential,
        CredentialType,
        Signature,
    )
    from rfc9420.messages.key_packages import LeafNode, LeafNodeSource

    cs = get_ciphersuite_by_id(DAVE_MLS_CIPHERSUITE_ID)
    if cs is None:
        raise ValueError(f"Unknown MLS ciphersuite id {DAVE_MLS_CIPHERSUITE_ID}")
    hpke_private, hpke_public = crypto.generate_key_pair()
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.primitives.asymmetric import ec

    sig_private = ec.generate_private_key(ec.SECP256R1())
    sig_private_der = sig_private.private_bytes(
        serialization.Encoding.DER,
        serialization.PrivateFormat.PKCS8,
        serialization.NoEncryption(),
    )
    sig_public_bytes = sig_private.public_key().public_bytes(
        serialization.Encoding.X962,
        serialization.PublicFormat.UncompressedPoint,
    )
    if sig_public_bytes[0] != 0x04:
        sig_public_bytes = b"\x04" + sig_public_bytes
    identity = user_id.to_bytes(8, "big")
    credential = Credential(
        identity=identity,
        public_key=sig_public_bytes,
        credential_type=CredentialType.BASIC,
    )
    from rfc9420.extensions.extensions import build_capabilities_data

    capabilities = build_capabilities_data(
        ciphersuite_ids=[DAVE_MLS_CIPHERSUITE_ID],
        supported_exts=[],
        include_grease=False,
    )
    lifetime_not_before = 0
    lifetime_not_after = (1 << 64) - 1
    group_id = session.group_id
    leaf_index = session.own_leaf_index
    leaf_node = LeafNode(
        encryption_key=hpke_public,
        signature_key=sig_public_bytes,
        credential=credential,
        capabilities=capabilities,
        leaf_node_source=LeafNodeSource.UPDATE,
        lifetime_not_before=lifetime_not_before,
        lifetime_not_after=lifetime_not_after,
        parent_hash=b"",
        extensions=[],
        signature=Signature(b""),
    )
    leaf_tbs = leaf_node.tbs_serialize(group_id=group_id, leaf_index=leaf_index)
    leaf_sig = crypto.sign(sig_private_der, leaf_tbs)
    leaf_node = LeafNode(
        encryption_key=hpke_public,
        signature_key=sig_public_bytes,
        credential=credential,
        capabilities=capabilities,
        leaf_node_source=LeafNodeSource.UPDATE,
        lifetime_not_before=lifetime_not_before,
        lifetime_not_after=lifetime_not_after,
        parent_hash=b"",
        extensions=[],
        signature=Signature(leaf_sig),
    )
    return session.update_self(leaf_node, signing_key_der)
