#!/usr/bin/env python3
"""Robuster Roborock-Singleton-Client für den Dispatcher-Server.

Hält eine persistente MQTT-Session aufrecht.
Bei Verbindungsabbruch oder Fehler: automatischer Reconnect.
Alle Calls haben einen äußeren asyncio-Timeout (CMD_TIMEOUT Sekunden).

Wichtig: Der Server nutzt ThreadingHTTPServer + asyncio.run() pro Thread.
Das bedeutet jeder Request bekommt eine neue Event-Loop. Der Client-State
(Manager + Device) wird über threading.Lock (kein asyncio.Lock) geschützt,
so dass mehrere Threads nicht gleichzeitig verbinden.

Verwendung:
    from roborock_client import RoomboyClient
    result = await RoomboyClient.run_cmd("GET_STATUS")
"""

import asyncio
import json
import logging
import pathlib
import threading
import time
import warnings

warnings.filterwarnings("ignore")

logger = logging.getLogger("roomboyClient")
logging.basicConfig(level=logging.WARNING)

_CACHE_FILE = pathlib.Path.home() / ".roborock_home_cache.json"
_CACHE_TTL = 3600  # 1 Stunde — HomeData bleibt gültig


class _FileCache:
    """Persistenter File-Cache für Roborock HomeData.
    Verhindert Cloud-Requests bei jedem Command (Rate-Limit: 5/Stunde).
    """

    def __init__(self):
        self._data = None  # in-memory nach erstem Laden

    async def get(self):
        from roborock.devices.cache import CacheData
        from roborock.data import HomeData
        if self._data is not None:
            return self._data
        if _CACHE_FILE.exists():
            try:
                raw = json.loads(_CACHE_FILE.read_text())
                ts = raw.get("_ts", 0)
                if time.time() - ts < _CACHE_TTL and raw.get("home_data"):
                    cd = CacheData()
                    cd.home_data = HomeData.from_dict(raw["home_data"])
                    self._data = cd
                    logger.info("HomeData aus Cache geladen (Alter: %ds)", int(time.time() - ts))
                    return cd
            except Exception as e:
                logger.warning("Cache-Lesen fehlgeschlagen: %s", e)
        self._data = CacheData()
        return self._data

    async def set(self, value):
        self._data = value
        try:
            hd = value.home_data
            if hd is not None:
                raw = {"_ts": time.time(), "home_data": hd.as_dict()}
                _CACHE_FILE.write_text(json.dumps(raw, default=str))
                logger.info("HomeData in Cache gespeichert")
        except Exception as e:
            logger.warning("Cache-Schreiben fehlgeschlagen: %s", e)


_file_cache = _FileCache()

# Zeitlimit pro Command in Sekunden (jede MQTT-Strategie hat bereits 10s intern)
CMD_TIMEOUT = 55.0  # Roombert braucht Zeit zum Aus-Dock-Fahren

_ROBOROCK_FILE = pathlib.Path.home() / ".roborock"
_CONFIG_FILE = pathlib.Path.home() / "Vibe Coding" / "dispatcher" / "hue" / "roborock_config.json"

# Thread-Lock schützt _manager/_device gegen gleichzeitigen Zugriff aus mehreren Threads
_state_lock = threading.Lock()
_manager = None
_device = None


def _load_credentials():
    """Liest UserData aus ~/.roborock und duid aus roborock_config.json."""
    cache = json.loads(_ROBOROCK_FILE.read_text())
    cfg = json.loads(_CONFIG_FILE.read_text())
    duid = cfg.get("devices", [{}])[0].get("duid", "")
    return cache, duid


async def _build_connection():
    """Baut neue DeviceManager-Verbindung auf und gibt (manager, device) zurück.
    Benutzt File-Cache für HomeData — vermeidet Cloud-Rate-Limit (5/Stunde).
    """
    from roborock import UserData
    from roborock.devices.device_manager import UserParams, create_device_manager

    cache, duid = _load_credentials()
    ud = UserData.from_dict(cache["user_data"])
    up = UserParams(username=cache["email"], user_data=ud)

    logger.info("Verbinde mit Roborock MQTT (Cache: %s) ...",
                "hit" if _file_cache._data and _file_cache._data.home_data else "miss")
    dm = await asyncio.wait_for(
        create_device_manager(up, cache=_file_cache),
        timeout=60.0,
    )
    devices = await asyncio.wait_for(dm.discover_devices(prefer_cache=True), timeout=60.0)

    if not devices:
        await dm.close()
        raise RuntimeError("Keine Roborock-Geräte gefunden")

    device = await dm.get_device(duid) or devices[0]
    logger.info("Roomboy verbunden (duid=%s)", duid)
    return dm, device


async def _close_manager(mgr):
    """Schließt Manager sauber."""
    try:
        await mgr.close()
    except Exception:
        pass


class RoomboyClient:
    """Namespace-Klasse mit statischen Hilfsmethoden.

    Da jeder HTTP-Request eine eigene asyncio Event-Loop hat (asyncio.run()),
    können wir den DeviceManager NICHT über Loop-Grenzen hinweg teilen.

    Stattdessen wird pro asyncio.run()-Call immer frisch verbunden.
    Das ist die zuverlässigste Methode für ThreadingHTTPServer.
    """

    def __init__(self):
        pass

    @staticmethod
    async def send_cmd(cmd_name: str, params=None) -> dict:
        """Sendet einen RoborockCommand per Name.

        Baut jedes Mal eine frische MQTT-Verbindung auf und schließt sie danach.
        Das ist bei ThreadingHTTPServer + asyncio.run() die robusteste Methode,
        da Event-Loops nicht über Threads geteilt werden können.

        Args:
            cmd_name: z.B. "GET_STATUS", "APP_SEGMENT_CLEAN", "APP_RC_START"
            params:   Optional — Liste oder None
        Returns:
            {"ok": True, "result": ...} oder {"ok": False, "error": ...}
        """
        from roborock import RoborockCommand

        cmd = getattr(RoborockCommand, cmd_name, None)
        if cmd is None:
            return {"ok": False, "error": f"Unbekannter Command: {cmd_name}"}

        dm = None
        try:
            dm, device = await _build_connection()
            prop = device.v1_properties
            if prop is None:
                return {"ok": False, "error": "Gerät unterstützt kein V1-Protokoll"}

            raw = await asyncio.wait_for(
                prop.command.send(cmd, params or []),
                timeout=CMD_TIMEOUT,
            )
            return {"ok": True, "result": str(raw) if raw is not None else "ok"}

        except Exception as e:
            logger.warning("send_cmd(%s) fehlgeschlagen: %s", cmd_name, e)
            return {"ok": False, "error": str(e)}
        finally:
            if dm is not None:
                await _close_manager(dm)

    @staticmethod
    async def get_status() -> dict:
        """Liest aktuellen Status via MQTT und gibt lesbares Dict zurück."""
        dm = None
        try:
            dm, device = await _build_connection()
            prop = device.v1_properties
            if prop is None:
                return {"ok": False, "error": "Kein V1-Protokoll"}

            await asyncio.wait_for(prop.status.refresh(), timeout=CMD_TIMEOUT)
            st = prop.status
            return {
                "ok": True,
                "state": st.state_name or str(st.state),
                "battery": st.battery,
                "error": st.error_code_name or str(st.error_code),
                "in_cleaning": st.in_cleaning,
                "in_returning": st.in_returning,
                "dock_state": str(st.dock_state),
            }
        except Exception as e:
            logger.warning("get_status fehlgeschlagen: %s", e)
            return {"ok": False, "error": str(e)}
        finally:
            if dm is not None:
                await _close_manager(dm)

    @staticmethod
    async def goto_room(segment_id: int) -> dict:
        """Schickt Roomboy in ein bestimmtes Zimmer (Segment-Reinigung).

        Args:
            segment_id: z.B. 41781032 für Wohnzimmer
        """
        params = [{"segments": [segment_id], "repeat": 1}]
        return await RoomboyClient.send_cmd("APP_SEGMENT_CLEAN", params)

    @staticmethod
    async def goto_target(x: int, y: int) -> dict:
        """Schickt Roomboy zu einem bestimmten Punkt auf der Karte.

        x, y: Koordinaten in mm im Roborock-Koordinatensystem (aus Kalibrierpunkten).
        Roomboy fährt hin und bleibt stehen — kein Saugen.
        """
        return await RoomboyClient.send_cmd("APP_GOTO_TARGET", [x, y])

    @staticmethod
    async def rc_start() -> dict:
        """Aktiviert Remote-Control-Modus."""
        return await RoomboyClient.send_cmd("APP_RC_START")

    @staticmethod
    async def rc_move(velocity: float = 0.0, omega: float = 0.0, duration: int = 1500) -> dict:
        """Bewegt Roomboy im RC-Modus.

        Args:
            velocity: Vorwärtsgeschwindigkeit in m/s (-0.3 bis 0.3)
            omega:    Drehgeschwindigkeit in rad/s (-1.5 bis 1.5)
            duration: Dauer in ms (Standard: 1500)
        """
        params = [{"omega": omega, "velocity": velocity, "duration": duration}]
        return await RoomboyClient.send_cmd("APP_RC_MOVE", params)

    @staticmethod
    async def rc_end() -> dict:
        """Beendet Remote-Control-Modus."""
        return await RoomboyClient.send_cmd("APP_RC_END")

    @classmethod
    def instance(cls):
        """Kompatibilitätsmethode — gibt die Klasse selbst zurück."""
        return cls
