import asyncio
from datetime import datetime
from typing import AsyncIterator

from aiospb.messages import MQTT5Client

# from dataclasses import dataclass, field
from aiospb.messages.ports import MQTTError
from aiospb.messages.values import MQTTMessage
from aiospb.nodes import DeviceConnectionIsBroken, MetricInfo, MetricsNetwork
from aiospb.shared import Clock, DeviceKey, ValueType


class Stack:
    def __init__(self):
        self._queue = asyncio.Queue()

    def put(self, item):
        self._queue.put_nowait(item)

    def __aiter__(self):
        return self

    async def __anext__(self):
        msg = await self._queue.get()
        if isinstance(msg, Exception):
            raise msg
        return msg


class _Ticker:
    def __init__(self, ts: int, interval: int):
        self.interval = interval
        self._ts = ts
        self._next_ts = (ts // interval) + 1 * interval
        self._event = asyncio.Event()

    def update(self, ts: int):
        if ts >= self._next_ts:
            self._event.set()
            self._next_ts += self.interval

        self._ts = ts

    def __aiter__(self):
        return self

    async def __anext__(self):
        await self._event.wait()
        self._event.clear()
        return self._next_ts - self.interval


class FakeClock(Clock):
    def __init__(self, ts_0=1000):
        self._ts = ts_0
        # self._sleep = None
        self._tickers = {}
        self.waits = []

    def now(self) -> datetime:
        return datetime.fromtimestamp(self._ts / 1000)

    def timestamp(self):
        return self._ts

    def sleep(self, milliseconds=1000):
        self._ts += milliseconds
        for ticker in self._tickers.values():
            ticker.update(self._ts)

    @property
    def ticker_times(self):
        return list(self._tickers.keys())

    async def asleep(self, milliseconds=1000):
        self.sleep(milliseconds)
        await asyncio.sleep(0.01)

    async def wait_for(self, task: asyncio.Task, timeout_ms: int):
        self.waits.append(timeout_ms)
        return await task

    def get_ticker(self, interval_ms: int) -> AsyncIterator:
        ticker = _Ticker(self._ts, interval_ms)
        self._tickers[interval_ms] = ticker
        return ticker


class FakeMQTTClient(MQTT5Client):
    """Fake implementation of MQTT5Client for acceptance tests"""

    def __init__(self):
        self.published = []
        self.stack = Stack()
        self.subscriptions = []
        self.connected = False
        self._blocked = None
        self.will = None

    async def connect(self, will: MQTTMessage):
        if self._blocked:
            raise self._blocked
        self.will = will
        self.connected = True

    def is_connected(self) -> bool:
        return self.connected

    async def disconnect(self):
        self.connected = False

    async def publish(self, message: MQTTMessage):
        if self._blocked:
            raise self._blocked
        self.published.append(message)

    async def subscribe(self, wildcard: str, qos: int):
        assert qos == 1
        self.subscriptions.append(wildcard)

    async def unsubscribe(self, wildcard: str):
        self.subscriptions.remove(wildcard)

    @property
    def messages(self) -> AsyncIterator[MQTTMessage]:
        return self.stack

    def block_connection(self):
        self._blocked = MQTTError(Exception("connection is blocked"))
        self.stack.put(self._blocked)

    def unblock_connection(self):
        self._blocked = None


def ts():
    return int(datetime.now().timestamp() * 1000)


class EasyMetricsNet(MetricsNetwork):
    """Very easy implementation of Metrics Network"""

    def __init__(self, info: dict[DeviceKey, list[MetricInfo]]):
        self._info = info
        self._readings = {key: {} for key in self._info.keys()}
        self.connected_devices = set()

    def get_device_keys(self) -> list[DeviceKey]:
        return list(self._info.keys())

    async def get_info(self, key: DeviceKey) -> list[MetricInfo]:
        return self._info[key]

    def _get_name(self, key, alias) -> str:
        if alias == 0:
            return ""
        for i in self._info[key]:
            if i.alias == alias:
                return i.name
        return ""

    async def read(self, key: DeviceKey, alias: int = 0, name: str = "") -> ValueType:
        if key not in self.connected_devices:
            raise DeviceConnectionIsBroken(key, "inmem")

        if key not in self._readings:
            return None

        if not name:
            name = self._get_name(key, alias)

        return self._readings[key].get(name, None)

    async def write(
        self, key: DeviceKey, value: ValueType, alias: int = 0, name: str = ""
    ) -> int:
        if key not in self.connected_devices:
            raise DeviceConnectionIsBroken(key, "inmem")

        if key not in self._readings:
            self._readings[key] = {}

        name = name if name else self._get_name(key, alias)
        self._readings[key][name] = value

        if alias:
            return alias

        for i in self._info[key]:
            if i.name == name:
                return i.alias

        return 0

    async def connect_device(self, key: DeviceKey):
        self.connected_devices.add(key)

    async def disconnect_device(self, key: DeviceKey):
        self.connected_devices.remove(key)
