"""Abstraction to send DescriptionRequest and wait for DescriptionResponse."""

from __future__ import annotations

from abc import ABC, abstractmethod
import asyncio
import logging
from typing import TYPE_CHECKING, Final

from xknx.exceptions import CommunicationError, XKNXException
from xknx.io import util
from xknx.io.gateway_scanner import GatewayDescriptor
from xknx.knxip import (
    HPAI,
    SRP,
    DescriptionRequest,
    DescriptionResponse,
    DIBTypeCode,
    KNXIPFrame,
    SearchRequestExtended,
    SearchResponseExtended,
)
from xknx.util import asyncio_timeout

from .const import DEFAULT_MCAST_PORT
from .transport import UDPTransport

if TYPE_CHECKING:
    from xknx.io.transport import KNXIPTransport

logger = logging.getLogger("xknx.log")

DESCRIPTION_TIMEOUT: Final = 2


async def request_description(
    gateway_ip: str,
    gateway_port: int = DEFAULT_MCAST_PORT,
    local_ip: str | None = None,
    local_port: int = 0,
    route_back: bool = False,
) -> GatewayDescriptor:
    """Set up a UDP transport to request a description from a KNXnet/IP device."""
    local_ip = local_ip or util.find_local_ip(gateway_ip)
    if local_ip is None:
        # Fall back to default interface and use route back
        local_ip = await util.get_default_local_ip(gateway_ip)
        if local_ip is None:
            raise CommunicationError(
                f"No network interface found to request gateway info from {gateway_ip}:{gateway_port}"
            )
        route_back = True
    try:
        local_ip = await util.validate_ip(local_ip, address_name="Local IP")
        gateway_ip = await util.validate_ip(gateway_ip, address_name="Gateway IP")
    except XKNXException as err:
        raise CommunicationError("Invalid address") from err

    transport = UDPTransport(
        local_addr=(local_ip, local_port),
        remote_addr=(gateway_ip, gateway_port),
        multicast=False,
    )
    try:
        await transport.connect()
    except OSError as err:
        raise CommunicationError(
            "Could not setup socket to request gateway info"
        ) from err
    else:
        local_hpai: HPAI
        if route_back:
            local_hpai = HPAI()
        else:
            local_addr = transport.getsockname()
            local_hpai = HPAI(*local_addr)

        description_query = DescriptionQuery(
            transport=transport,
            local_hpai=local_hpai,
        )
        await description_query.start()
        gateway = description_query.gateway_descriptor
        if gateway is None:
            raise CommunicationError(
                f"Could not fetch gateway info from {gateway_ip}:{gateway_port}"
            )
        if gateway.core_version >= 2:
            search_extended_query = SearchExtendedQuery(
                transport=transport,
                local_hpai=local_hpai,
            )
            await search_extended_query.start()
            gateway = search_extended_query.gateway_descriptor
            if gateway is None:
                raise CommunicationError(
                    f"Could not fetch extended gateway info from {gateway_ip}:{gateway_port}"
                )
        return gateway
    finally:
        transport.stop()


class _SelfDescriptionQuery(ABC):
    """Base class for handling descriptions request-response cycles."""

    expected_response_class: type[DescriptionResponse] | type[SearchResponseExtended]

    def __init__(
        self,
        transport: KNXIPTransport,
        local_hpai: HPAI,
    ) -> None:
        """Initialize Description class."""
        self.transport = transport
        self.local_hpai = local_hpai

        self.gateway_descriptor: GatewayDescriptor | None = None
        self.response_received_event = asyncio.Event()

    @abstractmethod
    def create_knxipframe(self) -> KNXIPFrame:
        """Create KNX/IP Frame object to be sent to device."""

    async def start(self) -> None:
        """Start. Send request and wait for an answer."""
        callb = self.transport.register_callback(
            self.response_rec_callback, [self.expected_response_class.SERVICE_TYPE]
        )
        frame = self.create_knxipframe()
        try:
            self.transport.send(frame)
            async with asyncio_timeout(DESCRIPTION_TIMEOUT):
                await self.response_received_event.wait()
        except asyncio.TimeoutError:
            logger.debug(
                "Error: KNX bus did not respond in time (%s secs) to request of type '%s'",
                DESCRIPTION_TIMEOUT,
                self.__class__.__name__,
            )
        except CommunicationError as err:
            logger.warning("Sending %s failed: %s", frame.body.__class__.__name__, err)
        finally:
            # cleanup to not leave callbacks (for asyncio.CancelledError)
            self.transport.unregister_callback(callb)

    def response_rec_callback(
        self, knxipframe: KNXIPFrame, source: HPAI, _: KNXIPTransport
    ) -> None:
        """Verify and handle knxipframe. Callback from internal transport."""
        if not isinstance(knxipframe.body, self.expected_response_class):
            logger.warning(
                "Wrong knxipframe for %s: %s", self.__class__.__name__, knxipframe
            )
            return
        self.response_received_event.set()
        # Set gateway descriptor attribute
        gateway = GatewayDescriptor(
            ip_addr=self.transport.remote_addr[0],
            port=self.transport.remote_addr[1],
            local_ip=self.transport.getsockname()[0],
        )
        gateway.parse_dibs(knxipframe.body.dibs)  # type: ignore[attr-defined]
        self.gateway_descriptor = gateway


class DescriptionQuery(_SelfDescriptionQuery):
    """Class to send a DescriptionRequest and wait for DescriptionResponse."""

    expected_response_class = DescriptionResponse

    def create_knxipframe(self) -> KNXIPFrame:
        """Create KNX/IP Frame object to be sent to device."""
        description_request = DescriptionRequest(control_endpoint=self.local_hpai)
        return KNXIPFrame.init_from_body(description_request)


class SearchExtendedQuery(_SelfDescriptionQuery):
    """
    Class to send a SearchRequestExtended and wait for SearchResponseExtended to a single device.

    Does only work with UDP transports.
    """

    expected_response_class = SearchResponseExtended

    def create_knxipframe(self) -> KNXIPFrame:
        """Create KNX/IP Frame object to be sent to device."""
        search_extended_request = SearchRequestExtended(
            discovery_endpoint=self.local_hpai,
            srps=[
                SRP.request_device_description(
                    [
                        DIBTypeCode.DEVICE_INFO,
                        DIBTypeCode.SUPP_SVC_FAMILIES,
                        DIBTypeCode.SECURED_SERVICE_FAMILIES,
                        DIBTypeCode.TUNNELING_INFO,
                    ]
                )
            ],
        )
        return KNXIPFrame.init_from_body(search_extended_request)
