# 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

"""
Backend Adapter for Twist Device Discovery

This module handles communication with the backend server to fetch device information.
It's designed to be easily replaceable if the server API changes.

The adapter maps backend device types to Twist model classes dynamically.
"""

import aiohttp
from typing import Optional, Type
from .TwistModel import TwistModel
from .TwistLight import TwistLight
from .TwistShutter import TwistShutter
from .TwistRgb import TwistRgb
from .TwistTemperature import TwistTemperature
from .TwistGarage import TwistGarage
from .TwistRelay import TwistRelay
from .TwistButton import TwistButton
from .TwistBinarySensor import TwistBinarySensor


class DeviceInfo:
    """Container for device information from backend"""

    def __init__(self, device_id: int, model_id: int, name: str, model_class: Type[TwistModel],
                 device_key: str, device_allocation_model_key: Optional[str] = None):
        self.device_id = device_id
        self.model_id = model_id
        self.name = name
        self.model_class = model_class
        self.device_key = device_key
        self.device_allocation_model_key = device_allocation_model_key


class ProductModelInfo:
    """Container for product model information"""

    def __init__(self, device_key: str, device_model_index: int, product_name: str,
                 product_model_label: str, product_model_alias: str):
        self.device_key = device_key
        self.device_model_index = device_model_index
        self.product_name = product_name
        self.product_model_label = product_model_label
        self.product_model_alias = product_model_alias


class SceneInfo:
    """Container for scene information"""

    def __init__(self, key: str, name: str, link_id: int, status: int):
        self.key = key
        self.name = name
        self.link_id = link_id
        self.status = status


class BackendAdapter:
    """
    Adapter for fetching device information from backend server.

    If the backend API changes, only this class needs to be updated.
    """

    def __init__(self, base_url: str, api_key: str, installation_uuid: str):
        """
        Initialize backend adapter

        Args:
            base_url: Base URL of the backend server (e.g., "https://backbone-dev.twist-innovation.com")
            api_key: API key for authentication (X-Api-Key header)
            installation_uuid: Installation UUID for the API endpoint
        """
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.installation_uuid = installation_uuid

    async def fetch_products(self) -> dict[str, ProductModelInfo]:
        """
        Fetch product information from backend server

        Returns:
            Dictionary mapping deviceAllocationModelKey -> ProductModelInfo

        Raises:
            aiohttp.ClientError: If request fails
        """
        url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}/products"
        headers = {
            "accept": "text/plain",
            "X-Api-Key": self.api_key
        }

        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers) as response:
                response.raise_for_status()
                data = await response.json()

        return self._parse_products(data)

    def _parse_products(self, data: dict) -> dict[str, ProductModelInfo]:
        """
        Parse products response into dictionary for easy lookup by model key

        Returns:
            Dictionary mapping deviceAllocationModelKey -> ProductModelInfo
        """
        result = {}

        for product in data.get("results", []):
            product_name = product.get("name", "")

            for model in product.get("models", []):
                model_key = model.get("key")
                device_key = model.get("deviceKey")
                device_model_index = model.get("deviceModelIndex")
                product_model_label = model.get("productModelLabel", "")
                product_model_alias = model.get("productModelAlias", "")

                if model_key:
                    result[model_key] = ProductModelInfo(
                        device_key=device_key,
                        device_model_index=device_model_index,
                        product_name=product_name,
                        product_model_label=product_model_label,
                        product_model_alias=product_model_alias
                    )

        return result

    async def fetch_installation_id(self) -> int:
        """
        Fetch installation ID from backend server

        Returns:
            Installation ID

        Raises:
            aiohttp.ClientError: If request fails
            ValueError: If response format is invalid
        """
        installation_data = await self.fetch_installation_info()
        return installation_data["installation_id"]

    async def fetch_installation_info(self) -> dict:
        """
        Fetch installation information from backend server

        Returns:
            Dictionary with 'installation_id' and 'installation_name'

        Raises:
            aiohttp.ClientError: If request fails
            ValueError: If response format is invalid
        """
        url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}"
        headers = {
            "accept": "text/plain",
            "X-Api-Key": self.api_key
        }

        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers) as response:
                response.raise_for_status()
                data = await response.json()

        installation = data.get("installation", {})
        installation_id = installation.get("installationId")
        installation_name = installation.get(
            "name", f"Twist Installation {self.installation_uuid[:8]}")

        if installation_id is None:
            raise ValueError(
                "Missing 'installationId' in installation response")

        return {
            "installation_id": installation_id,
            "installation_name": installation_name
        }

    async def fetch_config(self) -> list[DeviceInfo]:
        """
        Fetch device configuration from backend server

        Returns:
            List of DeviceInfo objects

        Raises:
            aiohttp.ClientError: If request fails
            ValueError: If response format is invalid
        """
        url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}/devices"
        headers = {
            "accept": "text/plain",
            "X-Api-Key": self.api_key
        }

        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers) as response:
                response.raise_for_status()
                data = await response.json()

        return self._parse_response(data)

    async def fetch_scenes(self) -> list[SceneInfo]:
        """
        Fetch scenes from backend server

        Returns:
            List of SceneInfo objects

        Raises:
            aiohttp.ClientError: If request fails
        """
        url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}/scenes"
        headers = {
            "accept": "text/plain",
            "X-Api-Key": self.api_key
        }

        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers) as response:
                response.raise_for_status()
                data = await response.json()

        return self._parse_scenes(data)

    def _parse_scenes(self, data: dict) -> list[SceneInfo]:
        """
        Parse scenes response into scene info list

        Args:
            data: JSON response from scenes endpoint

        Returns:
            List of SceneInfo objects
        """
        scene_list = []

        for scene in data.get("results", []):
            key = scene.get("key")
            name = scene.get("name")
            link_id = scene.get("linkId")
            status = scene.get("status")

            if key and name and link_id is not None and status is not None:
                scene_list.append(SceneInfo(key, name, link_id, status))

        return scene_list

    def _parse_response(self, data: dict) -> list[DeviceInfo]:
        """
        Parse backend response into device info list

        This method contains the mapping logic for the current API format.
        Update this method if the backend response format changes.

        Args:
            data: JSON response from backend (new format with results array)

        Returns:
            List of DeviceInfo objects
        """

        # Parse devices from new API format
        results = data.get("results", [])
        device_list = []

        for device in results:
            try:
                twist_id = device.get("twistId")
                device_key = device.get("key")
                models = device.get("models", [])

                # Process each model in the device
                for model in models:
                    model_index = model.get("deviceModelIndex")
                    model_type_name = model.get("modelTypeName", "")
                    label = model.get("label", f"Model {model_index}")
                    device_allocation_model_key = model.get(
                        "deviceAllocationModelKey")

                    # Map model type name directly to model class
                    model_class = self._map_model_type_to_class(
                        model_type_name)

                    if model_class:
                        device_info = DeviceInfo(
                            device_id=twist_id,
                            model_id=model_index,
                            name=label,
                            model_class=model_class,
                            device_key=device_key,
                            device_allocation_model_key=device_allocation_model_key
                        )
                        device_list.append(device_info)
                    else:
                        print(
                            f"Unknown model type '{model_type_name}' for device {twist_id}, model {model_index}")

            except (KeyError, TypeError) as e:
                print(f"Skipping device due to error: {e}")
                continue

        return device_list

    def _map_model_type_to_class(self, model_type_name: str) -> Optional[Type[TwistModel]]:
        """
        Map the API's modelTypeName directly to Twist model class

        Args:
            model_type_name: Model type name from the API

        Returns:
            TwistModel subclass or None if unmapped
        """
        mapping = {
            "Louvres": TwistShutter,
            "Mono Light": TwistLight,
            "RGB": TwistRgb,
            "Temperature": TwistTemperature,
            "CB Shutter": TwistShutter,
            "PB Shutter": TwistShutter,
            "Shutter": TwistShutter,
            "Garage": TwistGarage,
            "Relay": TwistRelay,
            "Button": TwistButton,
            "Binary Sensor": TwistBinarySensor,
            "Gateway": None,  # Skip Gateway models
            "Weather": None,  # Skip Weather models
            "Wind": None,  # Skip Wind models
            "Rain Sensor": None,  # Skip Rain Sensor models
            "Pergola Protection": None,  # Skip logic models
            "Unknown": None,  # Skip unknown models
        }

        return mapping.get(model_type_name)
