# Copyright 2021-2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
from __future__ import annotations

import asyncio
import collections
import dataclasses
import logging
import struct
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union, cast

from bumble import drivers, hci, utils
from bumble.colors import color
from bumble.core import ConnectionPHY, InvalidStateError, PhysicalTransport
from bumble.l2cap import L2CAP_PDU
from bumble.snoop import Snooper
from bumble.transport.common import TransportLostError

if TYPE_CHECKING:
    from bumble.transport.common import TransportSink, TransportSource


# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)


# -----------------------------------------------------------------------------
class DataPacketQueue(utils.EventEmitter):
    """
    Flow-control queue for host->controller data packets (ACL, ISO).

    The queue holds packets associated with a connection handle. The packets
    are sent to the controller, up to a maximum total number of packets in flight.
    A packet is considered to be "in flight" when it has been sent to the controller
    but not completed yet. Packets are no longer "in flight" when the controller
    declares them as completed.

    The queue emits a 'flow' event whenever one or more packets are completed.
    """

    max_packet_size: int

    class PerConnectionState:
        def __init__(self) -> None:
            self.in_flight = 0
            self.drained = asyncio.Event()

    def __init__(
        self,
        max_packet_size: int,
        max_in_flight: int,
        send: Callable[[hci.HCI_Packet], None],
    ) -> None:
        super().__init__()
        self.max_packet_size = max_packet_size
        self.max_in_flight = max_in_flight
        self._in_flight = 0  # Total number of packets in flight across all connections
        self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = (
            collections.defaultdict(DataPacketQueue.PerConnectionState)
        )
        self._drained_per_connection: dict[int, asyncio.Event] = (
            collections.defaultdict(asyncio.Event)
        )
        self._send = send
        self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
            collections.deque()
        )
        self._queued = 0
        self._completed = 0

    @property
    def queued(self) -> int:
        """Total number of packets queued since creation."""
        return self._queued

    @property
    def completed(self) -> int:
        """Total number of packets completed since creation."""
        return self._completed

    @property
    def pending(self) -> int:
        """Number of packets that have been queued but not completed."""
        return self._queued - self._completed

    def enqueue(self, packet: hci.HCI_Packet, connection_handle: int) -> None:
        """Enqueue a packet associated with a connection"""
        self._packets.appendleft((packet, connection_handle))
        self._queued += 1
        self._check_queue()

        if self._packets:
            logger.debug(
                f'{self._in_flight} packets in flight, '
                f'{len(self._packets)} in queue'
            )

    def flush(self, connection_handle: int) -> None:
        """
        Remove all packets associated with a connection.

        All packets associated with the connection that are in flight are implicitly
        marked as completed, but no 'flow' event is emitted.
        """

        packets_to_keep = [
            (packet, handle)
            for (packet, handle) in self._packets
            if handle != connection_handle
        ]
        if flushed_count := len(self._packets) - len(packets_to_keep):
            self._completed += flushed_count
            self._packets = collections.deque(packets_to_keep)

        if connection_state := self._connection_state.pop(connection_handle, None):
            in_flight = connection_state.in_flight
            self._completed += in_flight
            self._in_flight -= in_flight
            connection_state.drained.set()

    def _check_queue(self) -> None:
        while self._packets and self._in_flight < self.max_in_flight:
            packet, connection_handle = self._packets.pop()
            self._send(packet)
            self._in_flight += 1
            connection_state = self._connection_state[connection_handle]
            connection_state.in_flight += 1
            connection_state.drained.clear()

    def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
        """Mark one or more packets associated with a connection as completed."""
        if connection_handle not in self._connection_state:
            logger.warning(
                f'received completion for unknown connection {connection_handle}'
            )
            return

        connection_state = self._connection_state[connection_handle]
        if packet_count <= connection_state.in_flight:
            connection_state.in_flight -= packet_count
        else:
            logger.warning(
                f'{packet_count} completed for {connection_handle} '
                f'but only {connection_state.in_flight} in flight'
            )
            connection_state.in_flight = 0
        if connection_state.in_flight == 0:
            connection_state.drained.set()

        if packet_count <= self._in_flight:
            self._in_flight -= packet_count
            self._completed += packet_count
        else:
            logger.warning(
                f'{packet_count} completed but only {self._in_flight} in flight'
            )
            self._in_flight = 0
            self._completed = self._queued

        self._check_queue()
        self.emit('flow')

    async def drain(self, connection_handle: int) -> None:
        """Wait until there are no pending packets for a connection."""
        if not (connection_state := self._connection_state.get(connection_handle)):
            raise ValueError('no such connection')

        await connection_state.drained.wait()


# -----------------------------------------------------------------------------
class Connection:
    def __init__(
        self,
        host: Host,
        handle: int,
        peer_address: hci.Address,
        transport: PhysicalTransport,
    ):
        self.host = host
        self.handle = handle
        self.peer_address = peer_address
        self.assembler = hci.HCI_AclDataPacketAssembler(self.on_acl_pdu)
        self.transport = transport
        acl_packet_queue: Optional[DataPacketQueue] = (
            host.le_acl_packet_queue
            if transport == PhysicalTransport.LE
            else host.acl_packet_queue
        )
        assert acl_packet_queue
        self.acl_packet_queue = acl_packet_queue

    def on_hci_acl_data_packet(self, packet: hci.HCI_AclDataPacket) -> None:
        self.assembler.feed_packet(packet)

    def on_acl_pdu(self, pdu: bytes) -> None:
        l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
        self.host.on_l2cap_pdu(self, l2cap_pdu.cid, l2cap_pdu.payload)

    def __str__(self) -> str:
        return (
            f'Connection(transport={self.transport}, peer_address={self.peer_address})'
        )


# -----------------------------------------------------------------------------
@dataclasses.dataclass
class ScoLink:
    peer_address: hci.Address
    connection_handle: int


# -----------------------------------------------------------------------------
@dataclasses.dataclass
class IsoLink:
    handle: int
    packet_queue: DataPacketQueue = dataclasses.field(repr=False)
    packet_sequence_number: int = 0


# -----------------------------------------------------------------------------
class Host(utils.EventEmitter):
    connections: dict[int, Connection]
    cis_links: dict[int, IsoLink]
    bis_links: dict[int, IsoLink]
    sco_links: dict[int, ScoLink]
    bigs: dict[int, set[int]]
    acl_packet_queue: Optional[DataPacketQueue] = None
    le_acl_packet_queue: Optional[DataPacketQueue] = None
    iso_packet_queue: Optional[DataPacketQueue] = None
    hci_sink: Optional[TransportSink] = None
    hci_metadata: dict[str, Any]
    long_term_key_provider: Optional[
        Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
    ]
    link_key_provider: Optional[Callable[[hci.Address], Awaitable[Optional[bytes]]]]

    def __init__(
        self,
        controller_source: Optional[TransportSource] = None,
        controller_sink: Optional[TransportSink] = None,
    ) -> None:
        super().__init__()

        self.hci_metadata = {}
        self.ready = False  # True when we can accept incoming packets
        self.connections = {}  # Connections, by connection handle
        self.cis_links = {}  # CIS links, by connection handle
        self.bis_links = {}  # BIS links, by connection handle
        self.sco_links = {}  # SCO links, by connection handle
        self.bigs = {}  # BIG Handle to BIS Handles
        self.pending_command = None
        self.pending_response: Optional[asyncio.Future[Any]] = None
        self.number_of_supported_advertising_sets = 0
        self.maximum_advertising_data_length = 31
        self.local_version = None
        self.local_supported_commands = 0
        self.local_le_features = 0
        self.local_lmp_features = hci.LmpFeatureMask(0)  # Classic LMP features
        self.suggested_max_tx_octets = 251  # Max allowed
        self.suggested_max_tx_time = 2120  # Max allowed
        self.command_semaphore = asyncio.Semaphore(1)
        self.long_term_key_provider = None
        self.link_key_provider = None
        self.pairing_io_capability_provider = None  # Classic only
        self.snooper: Optional[Snooper] = None

        # Connect to the source and sink if specified
        if controller_source:
            self.set_packet_source(controller_source)
        if controller_sink:
            self.set_packet_sink(controller_sink)

    def find_connection_by_bd_addr(
        self,
        bd_addr: hci.Address,
        transport: Optional[int] = None,
        check_address_type: bool = False,
    ) -> Optional[Connection]:
        for connection in self.connections.values():
            if bytes(connection.peer_address) == bytes(bd_addr):
                if (
                    check_address_type
                    and connection.peer_address.address_type != bd_addr.address_type
                ):
                    continue
                if transport is None or connection.transport == transport:
                    return connection

        return None

    async def flush(self) -> None:
        # Make sure no command is pending
        await self.command_semaphore.acquire()

        # Flush current host state, then release command semaphore
        self.emit('flush')
        self.command_semaphore.release()

    async def reset(self, driver_factory=drivers.get_driver_for_host):
        if self.ready:
            self.ready = False
            await self.flush()

        # Instantiate and init a driver for the host if needed.
        # NOTE: we don't keep a reference to the driver here, because we don't
        # currently have a need for the driver later on. But if the driver interface
        # evolves, it may be required, then, to store a reference to the driver in
        # an object property.
        reset_needed = True
        if driver_factory is not None:
            if driver := await driver_factory(self):
                await driver.init_controller()
                reset_needed = False

        # Send a reset command unless a driver has already done so.
        if reset_needed:
            await self.send_command(hci.HCI_Reset_Command(), check_result=True)
            self.ready = True

        response = await self.send_command(
            hci.HCI_Read_Local_Supported_Commands_Command(), check_result=True
        )
        self.local_supported_commands = int.from_bytes(
            response.return_parameters.supported_commands, 'little'
        )

        if self.supports_command(hci.HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
            response = await self.send_command(
                hci.HCI_LE_Read_Local_Supported_Features_Command(), check_result=True
            )
            self.local_le_features = struct.unpack(
                '<Q', response.return_parameters.le_features
            )[0]

        if self.supports_command(hci.HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND):
            response = await self.send_command(
                hci.HCI_Read_Local_Version_Information_Command(), check_result=True
            )
            self.local_version = response.return_parameters

        if self.supports_command(hci.HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND):
            max_page_number = 0
            page_number = 0
            lmp_features = 0
            while page_number <= max_page_number:
                response = await self.send_command(
                    hci.HCI_Read_Local_Extended_Features_Command(
                        page_number=page_number
                    ),
                    check_result=True,
                )
                lmp_features |= int.from_bytes(
                    response.return_parameters.extended_lmp_features, 'little'
                ) << (64 * page_number)
                max_page_number = response.return_parameters.maximum_page_number
                page_number += 1
            self.local_lmp_features = hci.LmpFeatureMask(lmp_features)

        elif self.supports_command(hci.HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
            response = await self.send_command(
                hci.HCI_Read_Local_Supported_Features_Command(), check_result=True
            )
            self.local_lmp_features = hci.LmpFeatureMask(
                int.from_bytes(response.return_parameters.lmp_features, 'little')
            )

        await self.send_command(
            hci.HCI_Set_Event_Mask_Command(
                event_mask=hci.HCI_Set_Event_Mask_Command.mask(
                    [
                        hci.HCI_INQUIRY_COMPLETE_EVENT,
                        hci.HCI_INQUIRY_RESULT_EVENT,
                        hci.HCI_CONNECTION_COMPLETE_EVENT,
                        hci.HCI_CONNECTION_REQUEST_EVENT,
                        hci.HCI_DISCONNECTION_COMPLETE_EVENT,
                        hci.HCI_AUTHENTICATION_COMPLETE_EVENT,
                        hci.HCI_REMOTE_NAME_REQUEST_COMPLETE_EVENT,
                        hci.HCI_ENCRYPTION_CHANGE_EVENT,
                        hci.HCI_CHANGE_CONNECTION_LINK_KEY_COMPLETE_EVENT,
                        hci.HCI_LINK_KEY_TYPE_CHANGED_EVENT,
                        hci.HCI_READ_REMOTE_SUPPORTED_FEATURES_COMPLETE_EVENT,
                        hci.HCI_READ_REMOTE_VERSION_INFORMATION_COMPLETE_EVENT,
                        hci.HCI_QOS_SETUP_COMPLETE_EVENT,
                        hci.HCI_HARDWARE_ERROR_EVENT,
                        hci.HCI_FLUSH_OCCURRED_EVENT,
                        hci.HCI_ROLE_CHANGE_EVENT,
                        hci.HCI_MODE_CHANGE_EVENT,
                        hci.HCI_RETURN_LINK_KEYS_EVENT,
                        hci.HCI_PIN_CODE_REQUEST_EVENT,
                        hci.HCI_LINK_KEY_REQUEST_EVENT,
                        hci.HCI_LINK_KEY_NOTIFICATION_EVENT,
                        hci.HCI_LOOPBACK_COMMAND_EVENT,
                        hci.HCI_DATA_BUFFER_OVERFLOW_EVENT,
                        hci.HCI_MAX_SLOTS_CHANGE_EVENT,
                        hci.HCI_READ_CLOCK_OFFSET_COMPLETE_EVENT,
                        hci.HCI_CONNECTION_PACKET_TYPE_CHANGED_EVENT,
                        hci.HCI_QOS_VIOLATION_EVENT,
                        hci.HCI_PAGE_SCAN_REPETITION_MODE_CHANGE_EVENT,
                        hci.HCI_FLOW_SPECIFICATION_COMPLETE_EVENT,
                        hci.HCI_INQUIRY_RESULT_WITH_RSSI_EVENT,
                        hci.HCI_READ_REMOTE_EXTENDED_FEATURES_COMPLETE_EVENT,
                        hci.HCI_SYNCHRONOUS_CONNECTION_COMPLETE_EVENT,
                        hci.HCI_SYNCHRONOUS_CONNECTION_CHANGED_EVENT,
                        hci.HCI_SNIFF_SUBRATING_EVENT,
                        hci.HCI_EXTENDED_INQUIRY_RESULT_EVENT,
                        hci.HCI_ENCRYPTION_KEY_REFRESH_COMPLETE_EVENT,
                        hci.HCI_IO_CAPABILITY_REQUEST_EVENT,
                        hci.HCI_IO_CAPABILITY_RESPONSE_EVENT,
                        hci.HCI_USER_CONFIRMATION_REQUEST_EVENT,
                        hci.HCI_USER_PASSKEY_REQUEST_EVENT,
                        hci.HCI_REMOTE_OOB_DATA_REQUEST_EVENT,
                        hci.HCI_SIMPLE_PAIRING_COMPLETE_EVENT,
                        hci.HCI_LINK_SUPERVISION_TIMEOUT_CHANGED_EVENT,
                        hci.HCI_ENHANCED_FLUSH_COMPLETE_EVENT,
                        hci.HCI_USER_PASSKEY_NOTIFICATION_EVENT,
                        hci.HCI_KEYPRESS_NOTIFICATION_EVENT,
                        hci.HCI_REMOTE_HOST_SUPPORTED_FEATURES_NOTIFICATION_EVENT,
                        hci.HCI_LE_META_EVENT,
                    ]
                )
            )
        )
        if self.supports_command(hci.HCI_SET_EVENT_MASK_PAGE_2_COMMAND):
            await self.send_command(
                hci.HCI_Set_Event_Mask_Page_2_Command(
                    event_mask_page_2=hci.HCI_Set_Event_Mask_Page_2_Command.mask(
                        [hci.HCI_ENCRYPTION_CHANGE_V2_EVENT]
                    )
                )
            )

        if (
            self.local_version is not None
            and self.local_version.hci_version <= hci.HCI_VERSION_BLUETOOTH_CORE_4_0
        ):
            # Some older controllers don't like event masks with bits they don't
            # understand
            le_event_mask = bytes.fromhex('1F00000000000000')
        else:
            le_event_mask = hci.HCI_LE_Set_Event_Mask_Command.mask(
                [
                    hci.HCI_LE_CONNECTION_COMPLETE_EVENT,
                    hci.HCI_LE_ADVERTISING_REPORT_EVENT,
                    hci.HCI_LE_CONNECTION_UPDATE_COMPLETE_EVENT,
                    hci.HCI_LE_READ_REMOTE_FEATURES_COMPLETE_EVENT,
                    hci.HCI_LE_LONG_TERM_KEY_REQUEST_EVENT,
                    hci.HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_EVENT,
                    hci.HCI_LE_DATA_LENGTH_CHANGE_EVENT,
                    hci.HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT,
                    hci.HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT,
                    hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT,
                    hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
                    hci.HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT,
                    hci.HCI_LE_PHY_UPDATE_COMPLETE_EVENT,
                    hci.HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT,
                    hci.HCI_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_EVENT,
                    hci.HCI_LE_PERIODIC_ADVERTISING_REPORT_EVENT,
                    hci.HCI_LE_PERIODIC_ADVERTISING_SYNC_LOST_EVENT,
                    hci.HCI_LE_SCAN_TIMEOUT_EVENT,
                    hci.HCI_LE_ADVERTISING_SET_TERMINATED_EVENT,
                    hci.HCI_LE_SCAN_REQUEST_RECEIVED_EVENT,
                    hci.HCI_LE_CONNECTIONLESS_IQ_REPORT_EVENT,
                    hci.HCI_LE_CONNECTION_IQ_REPORT_EVENT,
                    hci.HCI_LE_CTE_REQUEST_FAILED_EVENT,
                    hci.HCI_LE_PERIODIC_ADVERTISING_SYNC_TRANSFER_RECEIVED_EVENT,
                    hci.HCI_LE_CIS_ESTABLISHED_EVENT,
                    hci.HCI_LE_CIS_REQUEST_EVENT,
                    hci.HCI_LE_CREATE_BIG_COMPLETE_EVENT,
                    hci.HCI_LE_TERMINATE_BIG_COMPLETE_EVENT,
                    hci.HCI_LE_BIG_SYNC_ESTABLISHED_EVENT,
                    hci.HCI_LE_BIG_SYNC_LOST_EVENT,
                    hci.HCI_LE_REQUEST_PEER_SCA_COMPLETE_EVENT,
                    hci.HCI_LE_PATH_LOSS_THRESHOLD_EVENT,
                    hci.HCI_LE_TRANSMIT_POWER_REPORTING_EVENT,
                    hci.HCI_LE_BIGINFO_ADVERTISING_REPORT_EVENT,
                    hci.HCI_LE_SUBRATE_CHANGE_EVENT,
                    hci.HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_EVENT,
                    hci.HCI_LE_CS_PROCEDURE_ENABLE_COMPLETE_EVENT,
                    hci.HCI_LE_CS_SECURITY_ENABLE_COMPLETE_EVENT,
                    hci.HCI_LE_CS_CONFIG_COMPLETE_EVENT,
                    hci.HCI_LE_CS_SUBEVENT_RESULT_EVENT,
                    hci.HCI_LE_CS_SUBEVENT_RESULT_CONTINUE_EVENT,
                ]
            )

        await self.send_command(
            hci.HCI_LE_Set_Event_Mask_Command(le_event_mask=le_event_mask)
        )

        if self.supports_command(hci.HCI_READ_BUFFER_SIZE_COMMAND):
            response = await self.send_command(
                hci.HCI_Read_Buffer_Size_Command(), check_result=True
            )
            hc_acl_data_packet_length = (
                response.return_parameters.hc_acl_data_packet_length
            )
            hc_total_num_acl_data_packets = (
                response.return_parameters.hc_total_num_acl_data_packets
            )

            logger.debug(
                'HCI ACL flow control: '
                f'hc_acl_data_packet_length={hc_acl_data_packet_length},'
                f'hc_total_num_acl_data_packets={hc_total_num_acl_data_packets}'
            )

            self.acl_packet_queue = DataPacketQueue(
                max_packet_size=hc_acl_data_packet_length,
                max_in_flight=hc_total_num_acl_data_packets,
                send=self.send_hci_packet,
            )

        le_acl_data_packet_length = 0
        total_num_le_acl_data_packets = 0
        iso_data_packet_length = 0
        total_num_iso_data_packets = 0
        if self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
            response = await self.send_command(
                hci.HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
            )
            le_acl_data_packet_length = (
                response.return_parameters.le_acl_data_packet_length
            )
            total_num_le_acl_data_packets = (
                response.return_parameters.total_num_le_acl_data_packets
            )
            iso_data_packet_length = response.return_parameters.iso_data_packet_length
            total_num_iso_data_packets = (
                response.return_parameters.total_num_iso_data_packets
            )

            logger.debug(
                'HCI LE flow control: '
                f'le_acl_data_packet_length={le_acl_data_packet_length},'
                f'total_num_le_acl_data_packets={total_num_le_acl_data_packets},'
                f'iso_data_packet_length={iso_data_packet_length},'
                f'total_num_iso_data_packets={total_num_iso_data_packets}'
            )
        elif self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_COMMAND):
            response = await self.send_command(
                hci.HCI_LE_Read_Buffer_Size_Command(), check_result=True
            )
            le_acl_data_packet_length = (
                response.return_parameters.le_acl_data_packet_length
            )
            total_num_le_acl_data_packets = (
                response.return_parameters.total_num_le_acl_data_packets
            )

            logger.debug(
                'HCI LE ACL flow control: '
                f'le_acl_data_packet_length={le_acl_data_packet_length},'
                f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
            )

        if le_acl_data_packet_length == 0 or total_num_le_acl_data_packets == 0:
            # LE and Classic share the same queue
            self.le_acl_packet_queue = self.acl_packet_queue
        else:
            # Create a separate queue for LE
            self.le_acl_packet_queue = DataPacketQueue(
                max_packet_size=le_acl_data_packet_length,
                max_in_flight=total_num_le_acl_data_packets,
                send=self.send_hci_packet,
            )

        if iso_data_packet_length and total_num_iso_data_packets:
            self.iso_packet_queue = DataPacketQueue(
                max_packet_size=iso_data_packet_length,
                max_in_flight=total_num_iso_data_packets,
                send=self.send_hci_packet,
            )

        if self.supports_command(
            hci.HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
        ) and self.supports_command(
            hci.HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
        ):
            response = await self.send_command(
                hci.HCI_LE_Read_Suggested_Default_Data_Length_Command()
            )
            suggested_max_tx_octets = response.return_parameters.suggested_max_tx_octets
            suggested_max_tx_time = response.return_parameters.suggested_max_tx_time
            if (
                suggested_max_tx_octets != self.suggested_max_tx_octets
                or suggested_max_tx_time != self.suggested_max_tx_time
            ):
                await self.send_command(
                    hci.HCI_LE_Write_Suggested_Default_Data_Length_Command(
                        suggested_max_tx_octets=self.suggested_max_tx_octets,
                        suggested_max_tx_time=self.suggested_max_tx_time,
                    )
                )

        if self.supports_command(
            hci.HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND
        ):
            response = await self.send_command(
                hci.HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command(),
                check_result=True,
            )
            self.number_of_supported_advertising_sets = (
                response.return_parameters.num_supported_advertising_sets
            )

        if self.supports_command(
            hci.HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND
        ):
            response = await self.send_command(
                hci.HCI_LE_Read_Maximum_Advertising_Data_Length_Command(),
                check_result=True,
            )
            self.maximum_advertising_data_length = (
                response.return_parameters.max_advertising_data_length
            )

    @property
    def controller(self) -> Optional[TransportSink]:
        return self.hci_sink

    @controller.setter
    def controller(self, controller) -> None:
        self.set_packet_sink(controller)
        if controller:
            self.set_packet_source(controller)

    def set_packet_sink(self, sink: Optional[TransportSink]) -> None:
        self.hci_sink = sink

    def set_packet_source(self, source: TransportSource) -> None:
        source.set_packet_sink(self)
        self.hci_metadata = getattr(source, 'metadata', self.hci_metadata)

    def send_hci_packet(self, packet: hci.HCI_Packet) -> None:
        logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {packet}')
        if self.snooper:
            self.snooper.snoop(bytes(packet), Snooper.Direction.HOST_TO_CONTROLLER)
        if self.hci_sink:
            self.hci_sink.on_packet(bytes(packet))

    async def send_command(
        self, command, check_result=False, response_timeout: Optional[int] = None
    ):
        # Wait until we can send (only one pending command at a time)
        async with self.command_semaphore:
            assert self.pending_command is None
            assert self.pending_response is None

            # Create a future value to hold the eventual response
            self.pending_response = asyncio.get_running_loop().create_future()
            self.pending_command = command

            try:
                self.send_hci_packet(command)
                await asyncio.wait_for(self.pending_response, timeout=response_timeout)
                response = self.pending_response.result()

                # Check the return parameters if required
                if check_result:
                    if isinstance(response, hci.HCI_Command_Status_Event):
                        status = response.status  # type: ignore[attr-defined]
                    elif isinstance(response.return_parameters, int):
                        status = response.return_parameters
                    elif isinstance(response.return_parameters, bytes):
                        # return parameters first field is a one byte status code
                        status = response.return_parameters[0]
                    else:
                        status = response.return_parameters.status

                    if status != hci.HCI_SUCCESS:
                        logger.warning(
                            f'{command.name} failed '
                            f'({hci.HCI_Constant.error_name(status)})'
                        )
                        raise hci.HCI_Error(status)

                return response
            except Exception:
                logger.exception(color("!!! Exception while sending command:", "red"))
                raise
            finally:
                self.pending_command = None
                self.pending_response = None

    # Use this method to send a command from a task
    def send_command_sync(self, command: hci.HCI_Command) -> None:
        async def send_command(command: hci.HCI_Command) -> None:
            await self.send_command(command)

        asyncio.create_task(send_command(command))

    def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
        if not (connection := self.connections.get(connection_handle)):
            logger.warning(f'connection 0x{connection_handle:04X} not found')
            return
        packet_queue = connection.acl_packet_queue
        if packet_queue is None:
            logger.warning(
                f'no ACL packet queue for connection 0x{connection_handle:04X}'
            )
            return

        # Create a PDU
        l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))

        # Send the data to the controller via ACL packets
        bytes_remaining = len(l2cap_pdu)
        offset = 0
        pb_flag = 0
        while bytes_remaining:
            data_total_length = min(bytes_remaining, packet_queue.max_packet_size)
            acl_packet = hci.HCI_AclDataPacket(
                connection_handle=connection_handle,
                pb_flag=pb_flag,
                bc_flag=0,
                data_total_length=data_total_length,
                data=l2cap_pdu[offset : offset + data_total_length],
            )
            logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
            packet_queue.enqueue(acl_packet, connection_handle)
            pb_flag = 1
            offset += data_total_length
            bytes_remaining -= data_total_length

    def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None:
        if connection := self.connections.get(connection_handle):
            return connection.acl_packet_queue

        if iso_link := self.cis_links.get(connection_handle) or self.bis_links.get(
            connection_handle
        ):
            return iso_link.packet_queue

        return None

    def send_iso_sdu(self, connection_handle: int, sdu: bytes) -> None:
        if not (
            iso_link := self.cis_links.get(connection_handle)
            or self.bis_links.get(connection_handle)
        ):
            logger.warning(f"no ISO link for connection handle {connection_handle}")
            return

        if iso_link.packet_queue is None:
            logger.warning("ISO link has no data packet queue")
            return

        bytes_remaining = len(sdu)
        offset = 0
        while bytes_remaining:
            is_first_fragment = offset == 0
            header_length = 4 if is_first_fragment else 0
            assert iso_link.packet_queue.max_packet_size > header_length
            fragment_length = min(
                bytes_remaining, iso_link.packet_queue.max_packet_size - header_length
            )
            is_last_fragment = bytes_remaining == fragment_length
            iso_sdu_fragment = sdu[offset : offset + fragment_length]
            iso_link.packet_queue.enqueue(
                (
                    hci.HCI_IsoDataPacket(
                        connection_handle=connection_handle,
                        data_total_length=header_length + fragment_length,
                        packet_sequence_number=iso_link.packet_sequence_number,
                        pb_flag=0b10 if is_last_fragment else 0b00,
                        packet_status_flag=0,
                        iso_sdu_length=len(sdu),
                        iso_sdu_fragment=iso_sdu_fragment,
                    )
                    if is_first_fragment
                    else hci.HCI_IsoDataPacket(
                        connection_handle=connection_handle,
                        data_total_length=fragment_length,
                        pb_flag=0b11 if is_last_fragment else 0b01,
                        iso_sdu_fragment=iso_sdu_fragment,
                    )
                ),
                connection_handle,
            )

            offset += fragment_length
            bytes_remaining -= fragment_length

        iso_link.packet_sequence_number = (iso_link.packet_sequence_number + 1) & 0xFFFF

    def remove_big(self, big_handle: int) -> None:
        if big := self.bigs.pop(big_handle, None):
            for connection_handle in big:
                if bis_link := self.bis_links.pop(connection_handle, None):
                    bis_link.packet_queue.flush(bis_link.handle)

    def supports_command(self, op_code: int) -> bool:
        return (
            self.local_supported_commands
            & hci.HCI_SUPPORTED_COMMANDS_MASKS.get(op_code, 0)
        ) != 0

    @property
    def supported_commands(self) -> set[int]:
        return set(
            op_code
            for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items()
            if self.local_supported_commands & mask
        )

    def supports_le_features(self, feature: hci.LeFeatureMask) -> bool:
        return (self.local_le_features & feature) == feature

    def supports_lmp_features(self, feature: hci.LmpFeatureMask) -> bool:
        return self.local_lmp_features & (feature) == feature

    @property
    def supported_le_features(self):
        return [
            feature for feature in range(64) if self.local_le_features & (1 << feature)
        ]

    # Packet Sink protocol (packets coming from the controller via HCI)
    def on_packet(self, packet: bytes) -> None:
        try:
            hci_packet = hci.HCI_Packet.from_bytes(packet)
        except Exception:
            logger.exception('!!! error parsing packet from bytes')
            return

        if self.ready or (
            isinstance(hci_packet, hci.HCI_Command_Complete_Event)
            and hci_packet.command_opcode == hci.HCI_RESET_COMMAND
        ):
            self.on_hci_packet(hci_packet)
        else:
            logger.debug(
                f'reset not done, ignoring packet from controller: {hci_packet}'
            )

    def on_transport_lost(self):
        # Called by the source when the transport has been lost.
        if self.pending_response:
            self.pending_response.set_exception(TransportLostError('transport lost'))

        self.emit('flush')

    def on_hci_packet(self, packet: hci.HCI_Packet) -> None:
        logger.debug(f'{color("### CONTROLLER -> HOST", "green")}: {packet}')

        if self.snooper:
            self.snooper.snoop(bytes(packet), Snooper.Direction.CONTROLLER_TO_HOST)

        # If the packet is a command, invoke the handler for this packet
        if packet.hci_packet_type == hci.HCI_COMMAND_PACKET:
            self.on_hci_command_packet(cast(hci.HCI_Command, packet))
        elif packet.hci_packet_type == hci.HCI_EVENT_PACKET:
            self.on_hci_event_packet(cast(hci.HCI_Event, packet))
        elif packet.hci_packet_type == hci.HCI_ACL_DATA_PACKET:
            self.on_hci_acl_data_packet(cast(hci.HCI_AclDataPacket, packet))
        elif packet.hci_packet_type == hci.HCI_SYNCHRONOUS_DATA_PACKET:
            self.on_hci_sco_data_packet(cast(hci.HCI_SynchronousDataPacket, packet))
        elif packet.hci_packet_type == hci.HCI_ISO_DATA_PACKET:
            self.on_hci_iso_data_packet(cast(hci.HCI_IsoDataPacket, packet))
        else:
            logger.warning(f'!!! unknown packet type {packet.hci_packet_type}')

    def on_hci_command_packet(self, command: hci.HCI_Command) -> None:
        logger.warning(f'!!! unexpected command packet: {command}')

    def on_hci_event_packet(self, event: hci.HCI_Event) -> None:
        handler_name = f'on_{event.name.lower()}'
        handler = getattr(self, handler_name, self.on_hci_event)
        handler(event)

    def on_hci_acl_data_packet(self, packet: hci.HCI_AclDataPacket) -> None:
        # Look for the connection to which this data belongs
        if connection := self.connections.get(packet.connection_handle):
            connection.on_hci_acl_data_packet(packet)

    def on_hci_sco_data_packet(self, packet: hci.HCI_SynchronousDataPacket) -> None:
        # Experimental
        self.emit('sco_packet', packet.connection_handle, packet)

    def on_hci_iso_data_packet(self, packet: hci.HCI_IsoDataPacket) -> None:
        # Experimental
        self.emit('iso_packet', packet.connection_handle, packet)

    def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
        self.emit('l2cap_pdu', connection.handle, cid, pdu)

    def on_command_processed(
        self, event: Union[hci.HCI_Command_Complete_Event, hci.HCI_Command_Status_Event]
    ):
        if self.pending_response:
            # Check that it is what we were expecting
            if self.pending_command is None:
                logger.warning('!!! pending_command is None ')
            elif self.pending_command.op_code != event.command_opcode:
                logger.warning(
                    '!!! command result mismatch, expected '
                    f'0x{self.pending_command.op_code:X} but got '
                    f'0x{event.command_opcode:X}'
                )

            self.pending_response.set_result(event)
        else:
            logger.warning('!!! no pending response future to set')

    ############################################################
    # HCI handlers
    ############################################################
    def on_hci_event(self, event: hci.HCI_Event):
        logger.warning(f'{color(f"--- Ignoring event {event}", "red")}')

    def on_hci_command_complete_event(self, event: hci.HCI_Command_Complete_Event):
        if event.command_opcode == 0:
            # This is used just for the Num_HCI_Command_Packets field, not related to
            # an actual command
            logger.debug('no-command event')
            return

        return self.on_command_processed(event)

    def on_hci_command_status_event(self, event: hci.HCI_Command_Status_Event):
        return self.on_command_processed(event)

    def on_hci_number_of_completed_packets_event(
        self, event: hci.HCI_Number_Of_Completed_Packets_Event
    ) -> None:
        for connection_handle, num_completed_packets in zip(
            event.connection_handles, event.num_completed_packets
        ):
            if queue := self.get_data_packet_queue(connection_handle):
                queue.on_packets_completed(num_completed_packets, connection_handle)
                continue

            if connection_handle not in self.sco_links:
                logger.warning(
                    'received packet completion event for unknown handle '
                    f'0x{connection_handle:04X}'
                )

    # Classic only
    def on_hci_connection_request_event(self, event: hci.HCI_Connection_Request_Event):
        # Notify the listeners
        self.emit(
            'connection_request',
            event.bd_addr,
            event.class_of_device,
            event.link_type,
        )

    def on_hci_le_connection_complete_event(
        self,
        event: Union[
            hci.HCI_LE_Connection_Complete_Event,
            hci.HCI_LE_Enhanced_Connection_Complete_Event,
            hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
        ],
    ):
        # Check if this is a cancellation
        if event.status == hci.HCI_SUCCESS:
            # Create/update the connection
            logger.debug(
                f'### LE CONNECTION: [0x{event.connection_handle:04X}] '
                f'{event.peer_address} as {hci.HCI_Constant.role_name(event.role)}'
            )

            connection = self.connections.get(event.connection_handle)
            if connection is None:
                connection = Connection(
                    self,
                    event.connection_handle,
                    event.peer_address,
                    PhysicalTransport.LE,
                )
                self.connections[event.connection_handle] = connection

            # Notify the client
            self.emit(
                'le_connection',
                event.connection_handle,
                event.peer_address,
                getattr(event, 'local_resolvable_private_address', None),
                getattr(event, 'peer_resolvable_private_address', None),
                hci.Role(event.role),
                event.connection_interval,
                event.peripheral_latency,
                event.supervision_timeout,
            )
        else:
            logger.debug(f'### CONNECTION FAILED: {event.status}')

            # Notify the listeners
            self.emit(
                'connection_failure',
                PhysicalTransport.LE,
                event.peer_address,
                event.status,
            )

    def on_hci_le_enhanced_connection_complete_event(
        self,
        event: Union[
            hci.HCI_LE_Enhanced_Connection_Complete_Event,
            hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
        ],
    ):
        # Just use the same implementation as for the non-enhanced event for now
        self.on_hci_le_connection_complete_event(event)

    def on_hci_le_enhanced_connection_complete_v2_event(
        self, event: hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
    ):
        # Just use the same implementation as for the v1 event for now
        self.on_hci_le_enhanced_connection_complete_event(event)

    def on_hci_connection_complete_event(
        self, event: hci.HCI_Connection_Complete_Event
    ):
        if event.status == hci.HCI_SUCCESS:
            # Create/update the connection
            logger.debug(
                f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] '
                f'{event.bd_addr}'
            )

            connection = self.connections.get(event.connection_handle)
            if connection is None:
                connection = Connection(
                    self,
                    event.connection_handle,
                    event.bd_addr,
                    PhysicalTransport.BR_EDR,
                )
                self.connections[event.connection_handle] = connection

            # Notify the client
            self.emit(
                'classic_connection',
                event.connection_handle,
                event.bd_addr,
            )
        else:
            logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')

            # Notify the client
            self.emit(
                'connection_failure',
                PhysicalTransport.BR_EDR,
                event.bd_addr,
                event.status,
            )

    def on_hci_disconnection_complete_event(
        self, event: hci.HCI_Disconnection_Complete_Event
    ):
        # Find the connection
        handle = event.connection_handle
        if (
            connection := (
                self.connections.get(handle)
                or self.cis_links.get(handle)
                or self.sco_links.get(handle)
            )
        ) is None:
            logger.warning('!!! DISCONNECTION COMPLETE: unknown handle')
            return

        if event.status == hci.HCI_SUCCESS:
            logger.debug(f'### DISCONNECTION: {connection}, reason={event.reason}')

            # Notify the listeners
            self.emit('disconnection', handle, event.reason)

            # Remove the handle reference
            _ = (
                self.connections.pop(handle, 0)
                or self.cis_links.pop(handle, 0)
                or self.sco_links.pop(handle, 0)
            )

            # Flush the data queues
            if self.acl_packet_queue:
                self.acl_packet_queue.flush(handle)
            if self.le_acl_packet_queue:
                self.le_acl_packet_queue.flush(handle)
            if self.iso_packet_queue:
                self.iso_packet_queue.flush(handle)
        else:
            logger.debug(f'### DISCONNECTION FAILED: {event.status}')

            # Notify the listeners
            self.emit('disconnection_failure', handle, event.status)

    def on_hci_le_connection_update_complete_event(
        self, event: hci.HCI_LE_Connection_Update_Complete_Event
    ):
        if (connection := self.connections.get(event.connection_handle)) is None:
            logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle')
            return

        # Notify the client
        if event.status == hci.HCI_SUCCESS:
            self.emit(
                'connection_parameters_update',
                connection.handle,
                event.connection_interval,
                event.peripheral_latency,
                event.supervision_timeout,
            )
        else:
            self.emit(
                'connection_parameters_update_failure', connection.handle, event.status
            )

    def on_hci_le_phy_update_complete_event(
        self, event: hci.HCI_LE_PHY_Update_Complete_Event
    ):
        if (connection := self.connections.get(event.connection_handle)) is None:
            logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle')
            return

        # Notify the client
        if event.status == hci.HCI_SUCCESS:
            self.emit(
                'connection_phy_update',
                connection.handle,
                ConnectionPHY(event.tx_phy, event.rx_phy),
            )
        else:
            self.emit('connection_phy_update_failure', connection.handle, event.status)

    def on_hci_le_advertising_report_event(
        self,
        event: (
            hci.HCI_LE_Advertising_Report_Event
            | hci.HCI_LE_Extended_Advertising_Report_Event
        ),
    ):
        for report in event.reports:
            self.emit('advertising_report', report)

    def on_hci_le_extended_advertising_report_event(
        self, event: hci.HCI_LE_Extended_Advertising_Report_Event
    ):
        self.on_hci_le_advertising_report_event(event)

    def on_hci_le_advertising_set_terminated_event(
        self, event: hci.HCI_LE_Advertising_Set_Terminated_Event
    ):
        self.emit(
            'advertising_set_termination',
            event.status,
            event.advertising_handle,
            event.connection_handle,
            event.num_completed_extended_advertising_events,
        )

    def on_hci_le_periodic_advertising_sync_established_event(
        self, event: hci.HCI_LE_Periodic_Advertising_Sync_Established_Event
    ):
        self.emit(
            'periodic_advertising_sync_establishment',
            event.status,
            event.sync_handle,
            event.advertising_sid,
            event.advertiser_address,
            event.advertiser_phy,
            event.periodic_advertising_interval,
            event.advertiser_clock_accuracy,
        )

    def on_hci_le_periodic_advertising_sync_lost_event(
        self, event: hci.HCI_LE_Periodic_Advertising_Sync_Lost_Event
    ):
        self.emit('periodic_advertising_sync_loss', event.sync_handle)

    def on_hci_le_periodic_advertising_report_event(
        self, event: hci.HCI_LE_Periodic_Advertising_Report_Event
    ):
        self.emit('periodic_advertising_report', event.sync_handle, event)

    def on_hci_le_biginfo_advertising_report_event(
        self, event: hci.HCI_LE_BIGInfo_Advertising_Report_Event
    ):
        self.emit('biginfo_advertising_report', event.sync_handle, event)

    def on_hci_le_cis_request_event(self, event: hci.HCI_LE_CIS_Request_Event):
        self.emit(
            'cis_request',
            event.acl_connection_handle,
            event.cis_connection_handle,
            event.cig_id,
            event.cis_id,
        )

    def on_hci_le_create_big_complete_event(
        self, event: hci.HCI_LE_Create_BIG_Complete_Event
    ):
        self.bigs[event.big_handle] = set(event.connection_handle)
        if self.iso_packet_queue is None:
            raise InvalidStateError("BIS established but ISO packets not supported")

        for connection_handle in event.connection_handle:
            self.bis_links[connection_handle] = IsoLink(
                connection_handle, self.iso_packet_queue
            )

        self.emit(
            'big_establishment',
            event.status,
            event.big_handle,
            event.connection_handle,
            event.big_sync_delay,
            event.transport_latency_big,
            event.phy,
            event.nse,
            event.bn,
            event.pto,
            event.irc,
            event.max_pdu,
            event.iso_interval,
        )

    def on_hci_le_big_sync_established_event(
        self, event: hci.HCI_LE_BIG_Sync_Established_Event
    ):
        self.bigs[event.big_handle] = set(event.connection_handle)
        if self.iso_packet_queue is None:
            raise InvalidStateError("BIS established but ISO packets not supported")

        for connection_handle in event.connection_handle:
            self.bis_links[connection_handle] = IsoLink(
                connection_handle, self.iso_packet_queue
            )

        self.emit(
            'big_sync_establishment',
            event.status,
            event.big_handle,
            event.transport_latency_big,
            event.nse,
            event.bn,
            event.pto,
            event.irc,
            event.max_pdu,
            event.iso_interval,
            event.connection_handle,
        )

    def on_hci_le_big_sync_lost_event(self, event: hci.HCI_LE_BIG_Sync_Lost_Event):
        self.remove_big(event.big_handle)
        self.emit('big_sync_lost', event.big_handle, event.reason)

    def on_hci_le_terminate_big_complete_event(
        self, event: hci.HCI_LE_Terminate_BIG_Complete_Event
    ):
        self.remove_big(event.big_handle)
        self.emit('big_termination', event.reason, event.big_handle)

    def on_hci_le_periodic_advertising_sync_transfer_received_event(
        self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_Event
    ):
        self.emit(
            'periodic_advertising_sync_transfer',
            event.status,
            event.connection_handle,
            event.sync_handle,
            event.advertising_sid,
            event.advertiser_address,
            event.advertiser_phy,
            event.periodic_advertising_interval,
            event.advertiser_clock_accuracy,
        )

    def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(
        self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_V2_Event
    ):
        self.emit(
            'periodic_advertising_sync_transfer',
            event.status,
            event.connection_handle,
            event.sync_handle,
            event.advertising_sid,
            event.advertiser_address,
            event.advertiser_phy,
            event.periodic_advertising_interval,
            event.advertiser_clock_accuracy,
        )

    def on_hci_le_cis_established_event(self, event: hci.HCI_LE_CIS_Established_Event):
        # The remaining parameters are unused for now.
        if event.status == hci.HCI_SUCCESS:
            if self.iso_packet_queue is None:
                raise InvalidStateError("CIS established but ISO packets not supported")
            self.cis_links[event.connection_handle] = IsoLink(
                handle=event.connection_handle, packet_queue=self.iso_packet_queue
            )
            self.emit(
                'cis_establishment',
                event.connection_handle,
                event.cig_sync_delay,
                event.cis_sync_delay,
                event.transport_latency_c_to_p,
                event.transport_latency_p_to_c,
                event.phy_c_to_p,
                event.phy_p_to_c,
                event.nse,
                event.bn_c_to_p,
                event.bn_p_to_c,
                event.ft_c_to_p,
                event.ft_p_to_c,
                event.max_pdu_c_to_p,
                event.max_pdu_p_to_c,
                event.iso_interval,
            )
        else:
            self.emit(
                'cis_establishment_failure', event.connection_handle, event.status
            )

    def on_hci_le_remote_connection_parameter_request_event(
        self, event: hci.HCI_LE_Remote_Connection_Parameter_Request_Event
    ):
        if event.connection_handle not in self.connections:
            logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle')
            return

        # For now, just accept everything
        # TODO: delegate the decision
        self.send_command_sync(
            hci.HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(
                connection_handle=event.connection_handle,
                interval_min=event.interval_min,
                interval_max=event.interval_max,
                max_latency=event.max_latency,
                timeout=event.timeout,
                min_ce_length=0,
                max_ce_length=0,
            )
        )

    def on_hci_le_long_term_key_request_event(
        self, event: hci.HCI_LE_Long_Term_Key_Request_Event
    ):
        if (connection := self.connections.get(event.connection_handle)) is None:
            logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle')
            return

        async def send_long_term_key():
            if self.long_term_key_provider is None:
                logger.debug('no long term key provider')
                long_term_key = None
            else:
                long_term_key = await utils.cancel_on_event(
                    self,
                    'flush',
                    # pylint: disable-next=not-callable
                    self.long_term_key_provider(
                        connection.handle,
                        event.random_number,
                        event.encryption_diversifier,
                    ),
                )
            if long_term_key:
                response = hci.HCI_LE_Long_Term_Key_Request_Reply_Command(
                    connection_handle=event.connection_handle,
                    long_term_key=long_term_key,
                )
            else:
                response = hci.HCI_LE_Long_Term_Key_Request_Negative_Reply_Command(
                    connection_handle=event.connection_handle
                )

            await self.send_command(response)

        asyncio.create_task(send_long_term_key())

    def on_hci_synchronous_connection_complete_event(
        self, event: hci.HCI_Synchronous_Connection_Complete_Event
    ):
        if event.status == hci.HCI_SUCCESS:
            # Create/update the connection
            logger.debug(
                f'### SCO CONNECTION: [0x{event.connection_handle:04X}] '
                f'{event.bd_addr}'
            )

            self.sco_links[event.connection_handle] = ScoLink(
                peer_address=event.bd_addr,
                connection_handle=event.connection_handle,
            )

            # Notify the client
            self.emit(
                'sco_connection',
                event.bd_addr,
                event.connection_handle,
                event.link_type,
            )
        else:
            logger.debug(f'### SCO CONNECTION FAILED: {event.status}')

            # Notify the client
            self.emit('sco_connection_failure', event.bd_addr, event.status)

    def on_hci_synchronous_connection_changed_event(
        self, event: hci.HCI_Synchronous_Connection_Changed_Event
    ):
        pass

    def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
        self.emit(
            'mode_change',
            event.connection_handle,
            event.status,
            event.current_mode,
            event.interval,
        )

    def on_hci_role_change_event(self, event: hci.HCI_Role_Change_Event):
        if event.status == hci.HCI_SUCCESS:
            logger.debug(
                f'role change for {event.bd_addr}: '
                f'{hci.HCI_Constant.role_name(event.new_role)}'
            )
            self.emit('role_change', event.bd_addr, hci.Role(event.new_role))
        else:
            logger.debug(
                f'role change for {event.bd_addr} failed: '
                f'{hci.HCI_Constant.error_name(event.status)}'
            )
            self.emit('role_change_failure', event.bd_addr, event.status)

    def on_hci_le_data_length_change_event(
        self, event: hci.HCI_LE_Data_Length_Change_Event
    ):
        if (connection := self.connections.get(event.connection_handle)) is None:
            logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
            return

        self.emit(
            'connection_data_length_change',
            event.connection_handle,
            event.max_tx_octets,
            event.max_tx_time,
            event.max_rx_octets,
            event.max_rx_time,
        )

    def on_hci_authentication_complete_event(
        self, event: hci.HCI_Authentication_Complete_Event
    ):
        # Notify the client
        if event.status == hci.HCI_SUCCESS:
            self.emit('connection_authentication', event.connection_handle)
        else:
            self.emit(
                'connection_authentication_failure',
                event.connection_handle,
                event.status,
            )

    def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event):
        # Notify the client
        if event.status == hci.HCI_SUCCESS:
            self.emit(
                'connection_encryption_change',
                event.connection_handle,
                event.encryption_enabled,
                0,
            )
        else:
            self.emit(
                'connection_encryption_failure', event.connection_handle, event.status
            )

    def on_hci_encryption_change_v2_event(
        self, event: hci.HCI_Encryption_Change_V2_Event
    ):
        # Notify the client
        if event.status == hci.HCI_SUCCESS:
            self.emit(
                'connection_encryption_change',
                event.connection_handle,
                event.encryption_enabled,
                event.encryption_key_size,
            )
        else:
            self.emit(
                'connection_encryption_failure', event.connection_handle, event.status
            )

    def on_hci_encryption_key_refresh_complete_event(
        self, event: hci.HCI_Encryption_Key_Refresh_Complete_Event
    ):
        # Notify the client
        if event.status == hci.HCI_SUCCESS:
            self.emit('connection_encryption_key_refresh', event.connection_handle)
        else:
            self.emit(
                'connection_encryption_key_refresh_failure',
                event.connection_handle,
                event.status,
            )

    def on_hci_qos_setup_complete_event(self, event: hci.HCI_QOS_Setup_Complete_Event):
        if event.status == hci.HCI_SUCCESS:
            self.emit(
                'connection_qos_setup', event.connection_handle, event.service_type
            )
        else:
            self.emit(
                'connection_qos_setup_failure',
                event.connection_handle,
                event.status,
            )

    def on_hci_link_supervision_timeout_changed_event(
        self, event: hci.HCI_Link_Supervision_Timeout_Changed_Event
    ):
        pass

    def on_hci_max_slots_change_event(self, event: hci.HCI_Max_Slots_Change_Event):
        pass

    def on_hci_page_scan_repetition_mode_change_event(
        self, event: hci.HCI_Page_Scan_Repetition_Mode_Change_Event
    ):
        pass

    def on_hci_link_key_notification_event(
        self, event: hci.HCI_Link_Key_Notification_Event
    ):
        logger.debug(
            f'link key for {event.bd_addr}: {event.link_key.hex()}, '
            f'type={hci.HCI_Constant.link_key_type_name(event.key_type)}'
        )
        self.emit('link_key', event.bd_addr, event.link_key, event.key_type)

    def on_hci_simple_pairing_complete_event(
        self, event: hci.HCI_Simple_Pairing_Complete_Event
    ):
        logger.debug(
            f'simple pairing complete for {event.bd_addr}: '
            f'status={hci.HCI_Constant.status_name(event.status)}'
        )
        if event.status == hci.HCI_SUCCESS:
            self.emit('classic_pairing', event.bd_addr)
        else:
            self.emit('classic_pairing_failure', event.bd_addr, event.status)

    def on_hci_pin_code_request_event(self, event: hci.HCI_PIN_Code_Request_Event):
        self.emit('pin_code_request', event.bd_addr)

    def on_hci_link_key_request_event(self, event: hci.HCI_Link_Key_Request_Event):
        async def send_link_key():
            if self.link_key_provider is None:
                logger.debug('no link key provider')
                link_key = None
            else:
                link_key = await utils.cancel_on_event(
                    self,
                    'flush',
                    # pylint: disable-next=not-callable
                    self.link_key_provider(event.bd_addr),
                )
            if link_key:
                response = hci.HCI_Link_Key_Request_Reply_Command(
                    bd_addr=event.bd_addr, link_key=link_key
                )
            else:
                response = hci.HCI_Link_Key_Request_Negative_Reply_Command(
                    bd_addr=event.bd_addr
                )

            await self.send_command(response)

        asyncio.create_task(send_link_key())

    def on_hci_io_capability_request_event(
        self, event: hci.HCI_IO_Capability_Request_Event
    ):
        self.emit('authentication_io_capability_request', event.bd_addr)

    def on_hci_io_capability_response_event(
        self, event: hci.HCI_IO_Capability_Response_Event
    ):
        self.emit(
            'authentication_io_capability_response',
            event.bd_addr,
            event.io_capability,
            event.authentication_requirements,
        )

    def on_hci_user_confirmation_request_event(
        self, event: hci.HCI_User_Confirmation_Request_Event
    ):
        self.emit(
            'authentication_user_confirmation_request',
            event.bd_addr,
            event.numeric_value,
        )

    def on_hci_user_passkey_request_event(
        self, event: hci.HCI_User_Passkey_Request_Event
    ):
        self.emit('authentication_user_passkey_request', event.bd_addr)

    def on_hci_user_passkey_notification_event(
        self, event: hci.HCI_User_Passkey_Notification_Event
    ):
        self.emit(
            'authentication_user_passkey_notification', event.bd_addr, event.passkey
        )

    def on_hci_inquiry_complete_event(self, _event: hci.HCI_Inquiry_Complete_Event):
        self.emit('inquiry_complete')

    def on_hci_inquiry_result_with_rssi_event(
        self, event: hci.HCI_Inquiry_Result_With_RSSI_Event
    ):
        for bd_addr, class_of_device, rssi in zip(
            event.bd_addr, event.class_of_device, event.rssi
        ):
            self.emit(
                'inquiry_result',
                bd_addr,
                class_of_device,
                b'',
                rssi,
            )

    def on_hci_extended_inquiry_result_event(
        self, event: hci.HCI_Extended_Inquiry_Result_Event
    ):
        self.emit(
            'inquiry_result',
            event.bd_addr,
            event.class_of_device,
            event.extended_inquiry_response,
            event.rssi,
        )

    def on_hci_remote_name_request_complete_event(
        self, event: hci.HCI_Remote_Name_Request_Complete_Event
    ):
        if event.status != hci.HCI_SUCCESS:
            self.emit('remote_name_failure', event.bd_addr, event.status)
        else:
            utf8_name = event.remote_name
            terminator = utf8_name.find(0)
            if terminator >= 0:
                utf8_name = utf8_name[0:terminator]

            self.emit('remote_name', event.bd_addr, utf8_name)

    def on_hci_remote_host_supported_features_notification_event(
        self, event: hci.HCI_Remote_Host_Supported_Features_Notification_Event
    ):
        self.emit(
            'remote_host_supported_features',
            event.bd_addr,
            event.host_supported_features,
        )

    def on_hci_le_read_remote_features_complete_event(
        self, event: hci.HCI_LE_Read_Remote_Features_Complete_Event
    ):
        if event.status != hci.HCI_SUCCESS:
            self.emit(
                'le_remote_features_failure', event.connection_handle, event.status
            )
        else:
            self.emit(
                'le_remote_features',
                event.connection_handle,
                int.from_bytes(event.le_features, 'little'),
            )

    def on_hci_le_cs_read_remote_supported_capabilities_complete_event(
        self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
    ):
        self.emit('cs_remote_supported_capabilities', event)

    def on_hci_le_cs_security_enable_complete_event(
        self, event: hci.HCI_LE_CS_Security_Enable_Complete_Event
    ):
        self.emit('cs_security', event)

    def on_hci_le_cs_config_complete_event(
        self, event: hci.HCI_LE_CS_Config_Complete_Event
    ):
        self.emit('cs_config', event)

    def on_hci_le_cs_procedure_enable_complete_event(
        self, event: hci.HCI_LE_CS_Procedure_Enable_Complete_Event
    ):
        self.emit('cs_procedure', event)

    def on_hci_le_cs_subevent_result_event(
        self, event: hci.HCI_LE_CS_Subevent_Result_Event
    ):
        self.emit('cs_subevent_result', event)

    def on_hci_le_cs_subevent_result_continue_event(
        self, event: hci.HCI_LE_CS_Subevent_Result_Continue_Event
    ):
        self.emit('cs_subevent_result_continue', event)

    def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
        self.emit(
            'le_subrate_change',
            event.connection_handle,
            event.subrate_factor,
            event.peripheral_latency,
            event.continuation_number,
            event.supervision_timeout,
        )

    def on_hci_vendor_event(self, event: hci.HCI_Vendor_Event):
        self.emit('vendor_event', event)
