from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import AsyncIterator, Iterable
from contextlib import suppress

from nibe.coil import Coil, CoilData
from nibe.exceptions import ReadException, ReadExceptionGroup, WriteDeniedException
from nibe.heatpump import HeatPump, ProductInfo, Series

DEFAULT_TIMEOUT: float = 5
READ_PRODUCT_INFO_TIMEOUT: float = 20  # Product info message is sent every 15 seconds


class Connection(ABC):
    """Base class for all connection methods."""

    async def start(self):  # noqa: B027
        """Start the connection."""
        pass

    async def stop(self):  # noqa: B027
        """Close the connection."""
        pass

    @abstractmethod
    async def read_coil(self, coil: Coil, timeout: float = DEFAULT_TIMEOUT) -> CoilData:
        """Read a coil data from the heatpump.

        :raises ReadIOException: If failed to read coil data due to IO error (will retry).
        :raises ReadException: If failed to read coil data due to other error (will not retry).
        """
        pass

    async def read_coils(
        self, coils: Iterable[Coil], timeout: float = DEFAULT_TIMEOUT
    ) -> AsyncIterator[CoilData]:
        """Read multiple coil data from the heatpump.

        :raises ReadExceptionGroup: If one or more coils failed to read."""
        exceptions = []
        for coil in coils:
            try:
                yield await self.read_coil(coil, timeout)
            except ReadException as exception:
                exceptions.append(exception)
        if exceptions:
            raise ReadExceptionGroup("Failed to read some or all coils", exceptions)

    @abstractmethod
    async def write_coil(
        self, coil_data: CoilData, timeout: float = DEFAULT_TIMEOUT
    ) -> None:
        """Write a coil data to the heatpump.

        :raises WriteIOException: If failed to write coil data due to IO error (will retry).
        :raises WriteException: If failed to write coil data due to other error (will not retry).
        """
        pass

    async def read_product_info(
        self, timeout: float = READ_PRODUCT_INFO_TIMEOUT
    ) -> ProductInfo:
        """Read product info from the heatpump.

        :raises ReadIOException: If failed to read product info in time."""
        raise NotImplementedError(
            "read_product_info method is not implemented for this connection method"
        )

    @abstractmethod
    async def verify_connectivity(self):
        """Verify that we have functioning communication.

        :raises NibeException: If failed to verify connectivity."""
        pass


async def verify_connectivity_read_write_alarm(
    connection: Connection, heatpump: HeatPump
):
    """Verify that we have functioning communication.

    To verify connection, we read the alarm reset field and write it as 0
    this will be ignored by the pump, this will throw exceptions on failure.

    We ignore if the write was denied since that is still indicative of a
    working connection. F-series pumps that is MyUplink upgraded seem to
    reject these writes.
    """
    if heatpump.series == Series.S:
        coil = heatpump.get_coil_by_name("reset-alarm-40023")
    else:
        coil = heatpump.get_coil_by_name("alarm-reset-45171")

    coil_data = await connection.read_coil(coil)
    value: str | int = 0
    if coil.mappings:
        value = coil.mappings[str(value)]
    coil_data.value = value

    with suppress(WriteDeniedException):
        await connection.write_coil(coil_data)
