from __future__ import annotations

import asyncio
import inspect
from collections.abc import Awaitable, Callable
from contextlib import suppress
from typing import Any

from proto_socket.communicator import Communicator
from proto_socket.packets.message_common_pb2 import HeartBeat


class _HbTimer:
    def __init__(self, delay: float, callback: Callable[[], Awaitable[None]]) -> None:
        self._task = asyncio.create_task(self._run(delay, callback))

    async def _run(self, delay: float, callback: Callable[[], Awaitable[None]]) -> None:
        await asyncio.sleep(delay)
        await callback()

    def cancel(self) -> None:
        self._task.cancel()


class BaseClient:
    def __init__(
        self,
        interval_sec: int,
        wait_sec: int,
        do_close: Callable[[], Awaitable[None]],
    ) -> None:
        self.communicator = Communicator()
        self._interval_sec = interval_sec
        self._wait_sec = wait_sec
        self._do_close = do_close
        self._close_called = False
        self._close_lock = asyncio.Lock()
        self._disconnect_listeners: list[Callable[[Any], Any]] = []
        self._hb_timer: _HbTimer | None = None
        self._waiting_hb_response = False

    def _init_base(self) -> None:
        self._close_lock = asyncio.Lock()

    async def close(self) -> None:
        await self._close_with_grace(True)

    async def _close_with_grace(self, graceful: bool) -> None:
        async with self._close_lock:
            if self._close_called:
                return
            self._close_called = True

        if graceful:
            await self.communicator.close()
        else:
            self.communicator.shutdown()

        self._stop_heartbeat()
        await self._do_close()
        await self._notify_disconnected()

    def add_disconnect_listener(self, fn: Callable[[Any], Any]) -> None:
        self._disconnect_listeners.append(fn)

    def remove_disconnect_listeners(self) -> None:
        self._disconnect_listeners.clear()

    async def _notify_disconnected(self) -> None:
        listeners = list(self._disconnect_listeners)
        self._disconnect_listeners.clear()
        for listener in listeners:
            result = listener(self)
            if inspect.isawaitable(result):
                await result

    async def _on_disconnected(self) -> None:
        await self._close_with_grace(False)

    async def _send_heartbeat(self) -> None:
        if not self.communicator.is_alive() or self._interval_sec <= 0:
            return
        self._stop_heartbeat()
        self._hb_timer = _HbTimer(self._interval_sec, self._heartbeat_interval_elapsed)

    async def _heartbeat_interval_elapsed(self) -> None:
        if not self.communicator.is_alive():
            return
        self._waiting_hb_response = True
        with suppress(Exception):
            await self.communicator.send(HeartBeat())
        self._stop_heartbeat()
        if self._wait_sec > 0:
            self._hb_timer = _HbTimer(self._wait_sec, self._heartbeat_wait_elapsed)

    async def _heartbeat_wait_elapsed(self) -> None:
        if self.communicator.is_alive():
            await self._on_disconnected()

    async def _on_heartbeat(self) -> None:
        if self._waiting_hb_response:
            self._waiting_hb_response = False
            return
        with suppress(Exception):
            await self.communicator.send(HeartBeat())

    def _stop_heartbeat(self) -> None:
        if self._hb_timer is not None:
            self._hb_timer.cancel()
            self._hb_timer = None
