from datetime import timedelta
from typing import Any, Callable, Optional, TypeVar, cast

import reactivex.operators as ops
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import event
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.device_registry import async_get as async_get_dr
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory
from homeassistant.util.dt import utcnow
from reactivex import Observable, Subject, compose, throw
from reactivex.subject.replaysubject import ReplaySubject

from . import ecoflow as ef
from .ecoflow import receive
from .ecoflow.rxtcp import RxTcpAutoConnection

CONF_PRODUCT = "product"
DISCONNECT_TIME = timedelta(seconds=15)
DOMAIN = "ecoflow"

_PLATFORMS = {
    Platform.BINARY_SENSOR,
    Platform.LIGHT,
    Platform.NUMBER,
    Platform.SELECT,
    Platform.SENSOR,
    Platform.SWITCH,
}

_T = TypeVar("_T")


async def to_task(src: Observable[_T]):
    return await src


async def request(tcp: RxTcpAutoConnection, req: bytes, res: Observable[_T]) -> _T:
    t = to_task(res.pipe(
        ops.timeout(5, throw(TimeoutError())),
        ops.first(),
    ))
    try:
        tcp.write(req)
    except BaseException as ex:
        t.close()
        raise ex
    return await t


def select_bms(idx: int):
    return compose(
        ops.filter(lambda x: x[0] == idx),
        ops.map(lambda x: cast(dict[str, Any], x[1])),
    )


class HassioEcoFlowClient:
    __disconnected = None
    __extra_connected = False

    def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
        self.tcp = RxTcpAutoConnection(entry.data[CONF_HOST], ef.PORT)
        self.product: int = entry.data[CONF_PRODUCT]
        self.serial = entry.unique_id
        self.diagnostics = dict[str, dict[str, Any]]()
        dr = async_get_dr(hass)

        self.device_info_main = DeviceInfo(
            identifiers={(DOMAIN, self.serial)},
            manufacturer="EcoFlow",
            name=entry.title,
        )
        if mac := entry.data.get(CONF_MAC, None):
            self.device_info_main["connections"] = {
                (CONNECTION_NETWORK_MAC, mac),
            }

        self.received = self.tcp.received.pipe(
            receive.merge_packet(),
            ops.map(receive.decode_packet),
            ops.share(),
        )
        self.pd = self.received.pipe(
            ops.filter(receive.is_pd),
            ops.map(lambda x: receive.parse_pd(x[3], self.product)),
            ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
            ops.ref_count(),
        )
        self.ems = self.received.pipe(
            ops.filter(receive.is_ems),
            ops.map(lambda x: receive.parse_ems(x[3], self.product)),
            ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
            ops.ref_count(),
        )
        self.inverter = self.received.pipe(
            ops.filter(receive.is_inverter),
            ops.map(lambda x: receive.parse_inverter(x[3], self.product)),
            ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
            ops.ref_count(),
        )
        self.mppt = self.received.pipe(
            ops.filter(receive.is_mppt),
            ops.map(lambda x: receive.parse_mppt(x[3], self.product)),
            ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
            ops.ref_count(),
        )
        self.bms = self.received.pipe(
            ops.filter(receive.is_bms),
            ops.map(lambda x: receive.parse_bms(x[3], self.product)),
            ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
            ops.ref_count(),
        )

        self.dc_in_current_config = self.received.pipe(
            ops.filter(receive.is_dc_in_current_config),
            ops.map(lambda x: receive.parse_dc_in_current_config(x[3])),
        )
        self.dc_in_type = self.received.pipe(
            ops.filter(receive.is_dc_in_type),
            ops.map(lambda x: receive.parse_dc_in_type(x[3])),
        )
        self.fan_auto = self.received.pipe(
            ops.filter(receive.is_fan_auto),
            ops.map(lambda x: receive.parse_fan_auto(x[3])),
        )
        self.lcd_timeout = self.received.pipe(
            ops.filter(receive.is_lcd_timeout),
            ops.map(lambda x: receive.parse_lcd_timeout(x[3])),
        )

        self.disconnected = Subject[Optional[int]]()

        def _disconnected(*args):
            self.__disconnected = None
            self.tcp.reconnect()
            self.diagnostics.clear()
            self.disconnected.on_next(None)
            if self.__extra_connected:
                self.__extra_connected = False

        def reset_timer(*args):
            if self.__disconnected:
                self.__disconnected()
            self.__disconnected = event.async_track_point_in_utc_time(
                hass,
                _disconnected,
                utcnow().replace(microsecond=0) + (DISCONNECT_TIME + timedelta(seconds=1)),
            )

        def end_timer(ex=None):
            self.disconnected.on_next(None)
            if ex:
                self.disconnected.on_error(ex)
            else:
                self.disconnected.on_completed()
        self.received.subscribe(reset_timer, end_timer, end_timer)

        def pd_updated(data: dict[str, Any]):
            self.diagnostics["pd"] = data
            self.device_info_main["model"] = ef.get_model_name(
                self.product, data["model"])
            dr.async_get_or_create(
                config_entry_id=entry.entry_id,
                **self.device_info_main,
            )
            if self.__extra_connected != ef.has_extra(self.product, data.get("model", None)):
                self.__extra_connected = not self.__extra_connected
                if not self.__extra_connected:
                    self.disconnected.on_next(1)
        self.pd.subscribe(pd_updated)

        def bms_updated(data: tuple[int, dict[str, Any]]):
            if "bms" not in self.diagnostics:
                self.diagnostics["bms"] = dict[str, Any]()
            self.diagnostics["bms"][data[0]] = data[1]
        self.bms.subscribe(bms_updated)

        def ems_updated(data: dict[str, Any]):
            self.diagnostics["ems"] = data
        self.ems.subscribe(ems_updated)

        def inverter_updated(data: dict[str, Any]):
            self.diagnostics["inverter"] = data
        self.inverter.subscribe(inverter_updated)

        def mppt_updated(data: dict[str, Any]):
            self.diagnostics["mppt"] = data
        self.mppt.subscribe(mppt_updated)

    async def close(self):
        self.tcp.close()
        await self.tcp.wait_closed()


class EcoFlowBaseEntity(Entity):
    _attr_has_entity_name = True
    _attr_should_poll = False
    _connected = False

    def __init__(self, client: HassioEcoFlowClient, bms_id: Optional[int] = None):
        self._attr_available = False
        self._client = client
        self._bms_id = bms_id or 0
        self._attr_device_info = client.device_info_main
        self._attr_unique_id = client.serial
        if bms_id:
            self._attr_unique_id += f"-{bms_id}"

    async def async_added_to_hass(self):
        await super().async_added_to_hass()
        self._subscribe(self._client.disconnected, self.__on_disconnected)

    def _subscribe(self, src: Observable, func: Callable):
        self.async_on_remove(src.subscribe(func).dispose)

    def __on_disconnected(self, bms_id: Optional[int]):
        if bms_id is not None and self._bms_id != bms_id:
            return
        self._connected = False
        if self._attr_available:
            self._attr_available = False
            self.async_write_ha_state()


class EcoFlowEntity(EcoFlowBaseEntity):
    def __init__(self, client: HassioEcoFlowClient, src: Observable[dict[str, Any]], key: str, name: str, bms_id: Optional[int] = None):
        super().__init__(client, bms_id)
        self._key = key
        self._src = src
        self._attr_name = name
        self._attr_unique_id += f"-{key.replace('_', '-')}"

    async def async_added_to_hass(self):
        await super().async_added_to_hass()
        self._subscribe(self._src, self.__updated)

    def __updated(self, data: dict[str, Any]):
        self._attr_available = True
        self._on_updated(data)
        self.async_write_ha_state()

    def _on_updated(self, data: dict[str, Any]):
        pass


class EcoFlowConfigEntity(EcoFlowBaseEntity):
    _attr_entity_category = EntityCategory.CONFIG
    _attr_should_poll = True

    def __init__(self, client: HassioEcoFlowClient, key: str, name: str):
        super().__init__(client)
        self._attr_name = name
        self._attr_unique_id += f"-{key.replace('_', '-')}"

    async def async_added_to_hass(self):
        await super().async_added_to_hass()
        self._subscribe(self._client.received, self.__updated)

    def __updated(self, data):
        if not self._connected:
            self._connected = True
            self.async_schedule_update_ha_state(True)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
    if DOMAIN not in hass.data:
        hass.data[DOMAIN] = {}

    client = HassioEcoFlowClient(hass, entry)

    hass.data[DOMAIN][entry.entry_id] = client
    hass.config_entries.async_setup_platforms(entry, _PLATFORMS)
    return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
    if not await hass.config_entries.async_unload_platforms(entry, _PLATFORMS):
        return False

    client: HassioEcoFlowClient = hass.data[DOMAIN].pop(entry.entry_id)
    await client.close()
    return True
