# Copyright (C) 2025 Twist Innovation
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See the GNU General Public License for more details:
# https://www.gnu.org/licenses/gpl-3.0.html


import asyncio
import json
import random
from typing import Optional

from .TwistDevice import TwistDevice, TwistModel
from .BackendAdapter import BackendAdapter
from .TwistScene import TwistScene


class TwistAPI:
    def __init__(self, backend_url: str, api_key: str, installation_uuid: str):
        """
        Initialize TwistAPI

        Args:
            backend_url: Backend server URL (e.g., "https://backbone-dev.twist-innovation.com")
            api_key: API key for backend authentication
            installation_uuid: Installation UUID for the backend API
        """
        self._ext_publish = None
        self._subscribe = None

        self.function_map = {
            "context": self._context_msg,
            "getboard": self._get_board
        }

        self.device_list: list[TwistDevice] = list()
        self.scene_list: list[TwistScene] = list()
        self.scene_map: dict[str, TwistScene] = {}
        self.scene_name_map: dict[str, TwistScene] = {}
        self.installation_id: Optional[int] = None
        self.installation_name: Optional[str] = None

        # Backend adapter for fetching device info
        self._backend_adapter = BackendAdapter(
            backend_url, api_key, installation_uuid)

    def set_mqtt_publish(self, publisher):
        """Set MQTT publish function so entities can send messages during registration."""
        self._ext_publish = publisher

    async def set_mqtt_subscribe(self, subscriber):
        """Set MQTT subscribe function and start receiving messages."""
        self._subscribe = subscriber
        await self._subscribe(f"v2/{self.installation_id}/rx/#", self._on_message_received)

    async def add_mqtt(self, publisher, subscriber):
        """Legacy method for backward compatibility - sets both publish and subscribe."""
        self.set_mqtt_publish(publisher)
        await self.set_mqtt_subscribe(subscriber)

    async def get_models(self):
        """
        Get models from backend

        Returns:
            List of TwistModel objects
        """
        # Fetch installation info from API
        installation_info = await self._backend_adapter.fetch_installation_info()
        self.installation_id = installation_info["installation_id"]
        self.installation_name = installation_info["installation_name"]

        # Fetch product info for naming
        products = await self._backend_adapter.fetch_products()

        # Fetch scenes once during boot
        scene_infos = await self._backend_adapter.fetch_scenes()
        self.scene_list = []
        self.scene_map = {}
        self.scene_name_map = {}

        for scene_info in scene_infos:
            scene = TwistScene(
                key=scene_info.key,
                name=scene_info.name,
                link_id=scene_info.link_id,
                status=scene_info.status,
                api=self
            )
            self.scene_list.append(scene)
            self.scene_map[scene.key] = scene
            self.scene_name_map[scene.name] = scene

        # Fetch device info from API
        device_infos = await self._backend_adapter.fetch_config()

        # Group devices by device_id
        devices_dict = {}
        for device_info in device_infos:
            if device_info.device_id not in devices_dict:
                devices_dict[device_info.device_id] = []
            devices_dict[device_info.device_id].append(device_info)

        # Create TwistDevice objects with models
        model_list: list[TwistModel] = list()
        for device_id, infos in devices_dict.items():
            # Sort by model_id to ensure correct order
            infos.sort(key=lambda x: x.model_id)

            # Create device if it doesn't exist
            device = next(
                (d for d in self.device_list if d.twist_id == device_id), None)
            if device is None:
                device = TwistDevice(device_id, 0, self)
                self.device_list.append(device)

            # Create models for this device
            for info in infos:
                model = info.model_class(info.model_id, device)

                # Get product info if available using deviceAllocationModelKey
                product_info = None
                if info.device_allocation_model_key:
                    product_info = products.get(
                        info.device_allocation_model_key)

                if product_info:
                    model.product_name = product_info.product_name
                    model.name = product_info.product_model_label or product_info.product_model_alias or info.name
                else:
                    model.product_name = None
                    model.name = info.name

                # Add model at correct position in model_list
                while len(device.model_list) <= info.model_id:
                    device.model_list.append(None)
                device.model_list[info.model_id] = model

                model_list.append(model)

        return model_list

    async def _publish(self, twist_id, opcode, payload: dict | None = None, model_id=None):
        if self._ext_publish is not None:
            topic = f"v2/{self.installation_id}/tx/{twist_id}/{opcode}"
            if payload is None:
                payload = dict()
            payload["key"] = random.randint(1, 65535)
            if model_id is not None:
                topic = f"{topic}/{model_id}"

            await self._ext_publish(topic, json.dumps(payload))

    async def getboard(self, twist_id):
        await self._publish(twist_id, "getboard")

    async def activate_event(self, model: TwistModel, data: json):
        await self._publish(model.parent_device.twist_id, "activate_event", data, model.model_id)

    async def activate_scene(self, scene: TwistScene):
        await self._publish(4294967295, "activatescene", {"l": scene.link_id})

    async def activate_scene_by_key(self, scene_key: str):
        scene = self.scene_map.get(scene_key)
        if scene is None:
            raise ValueError(f"Scene with key '{scene_key}' not found")
        await self.activate_scene(scene)

    async def activate_scene_by_name(self, scene_name: str):
        scene = self.scene_name_map.get(scene_name)
        if scene is None:
            raise ValueError(f"Scene with name '{scene_name}' not found")
        await self.activate_scene(scene)

    async def request_context(self, model: TwistModel):
        await self._publish(model.parent_device.twist_id, "context", model_id=model.model_id)

    def _parse_topic(self, topic):
        tpc_delim = topic.split('/')

        model_id = None
        if len(tpc_delim) == 6:
            model_id = int(tpc_delim[5])

        return {
            "twist_id": int(tpc_delim[3]),
            "opcode": tpc_delim[4],
            "model_id": model_id
        }

    async def _on_message_received(self, topic, payload, qos=None):
        data = self._parse_topic(topic)
        if any(dev.twist_id == data["twist_id"] for dev in self.device_list) or data["opcode"] == "getboard":
            if data["opcode"] in self.function_map:
                await self.function_map[data["opcode"]](data["twist_id"], payload, data["model_id"])

    async def _context_msg(self, twist_id, payload, model_id):
        device = next(
            (d for d in self.device_list if d.twist_id == twist_id), None)
        await device.context_msg(model_id, payload)

    async def _get_board(self, twist_id, payload, model_id=None):
        """Handle getboard MQTT message (legacy compatibility)"""
        pass
