#!/usr/bin/env python3
"""
Somfy Tahoma Local API to MySQL Database Script
Liest alle Aktoren und Sensoren aus der Tahoma Box und speichert sie in MySQL
"""

import requests
import pymysql
from pymysql import Error
import json
import logging
from typing import List, Dict, Optional
import urllib3
import socket
import configparser
import os
from mqtt_discovery import HomeAssistantDiscovery, MQTTDeviceConverter

# SSL-Warnungen deaktivieren (Tahoma verwendet selbst-signierte Zertifikate)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class Config:
    """Lädt und verwaltet die Konfiguration aus config.ini"""
    
    def __init__(self, config_file: str = 'config.ini'):
        """
        Lädt die Konfiguration
        
        Args:
            config_file: Name der Konfigurationsdatei (wird im Script-Verzeichnis gesucht)
        """
        # Verzeichnis des Scripts ermitteln
        script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(script_dir, config_file)
        
        if not os.path.exists(config_path):
            raise FileNotFoundError(
                f"Konfigurationsdatei '{config_file}' nicht gefunden!\n"
                f"Erwartet in: {script_dir}\n"
                f"Bitte erstellen Sie die Datei anhand der Vorlage config.ini.example"
            )
        
        self.config = configparser.ConfigParser()
        self.config.read(config_path, encoding='utf-8')
        
        logger.info(f"Konfiguration geladen von: {config_path}")
        
        # Konfiguration validieren
        self._validate()
    
    def _validate(self):
        """Validiert die Konfiguration"""
        required_sections = ['tahoma', 'database', 'wled', 'options']
        for section in required_sections:
            if not self.config.has_section(section):
                raise ValueError(f"Erforderliche Sektion '[{section}]' fehlt in config.ini")
    
    def _get_bool(self, section: str, key: str, fallback: bool = False) -> bool:
        """Hilfsmethode zum Lesen von Boolean-Werten"""
        value = self.config.get(section, key, fallback=str(fallback)).strip().lower()
        return value in ('true', '1', 'yes', 'on')
    
    def _get_int(self, section: str, key: str, fallback: int = 0) -> int:
        """Hilfsmethode zum Lesen von Integer-Werten"""
        try:
            return self.config.getint(section, key, fallback=fallback)
        except ValueError:
            return fallback
    
    def _get_list(self, section: str, key: str) -> List[str]:
        """Hilfsmethode zum Lesen von Listen (komma-getrennt)"""
        value = self.config.get(section, key, fallback='').strip()
        if not value:
            return []
        return [item.strip() for item in value.split(',') if item.strip()]
    
    # Tahoma Konfiguration
    @property
    def tahoma_ip(self) -> str:
        return self.config.get('tahoma', 'ip').strip()
    
    @property
    def tahoma_token(self) -> str:
        return self.config.get('tahoma', 'token').strip()
    
    @property
    def tahoma_timeout(self) -> int:
        if self.config.has_section('advanced'):
            return self._get_int('advanced', 'tahoma_timeout', 10)
        return 10
    
    # Datenbank Konfiguration
    @property
    def db_host(self) -> str:
        return self.config.get('database', 'host').strip()
    
    @property
    def db_port(self) -> int:
        return self._get_int('database', 'port', 3306)
    
    @property
    def db_name(self) -> str:
        return self.config.get('database', 'database').strip()
    
    @property
    def db_user(self) -> str:
        return self.config.get('database', 'user').strip()
    
    @property
    def db_password(self) -> str:
        return self.config.get('database', 'password').strip()
    
    # WLED Konfiguration
    @property
    def wled_enable(self) -> bool:
        return self._get_bool('wled', 'enable', True)
    
    @property
    def wled_discovery_timeout(self) -> int:
        return self._get_int('wled', 'discovery_timeout', 5)
    
    @property
    def wled_manual_ips(self) -> List[str]:
        return self._get_list('wled', 'manual_ips')
    
    @property
    def wled_scan_network(self) -> Optional[str]:
        value = self.config.get('wled', 'scan_network', fallback='').strip()
        return value if value else None
    
    @property
    def wled_timeout(self) -> int:
        if self.config.has_section('advanced'):
            return self._get_int('advanced', 'wled_timeout', 2)
        return 2
    # MQTT Konfiguration
    @property
    def mqtt_enable(self) -> bool:
        if not self.config.has_section('mqtt'):
            return False
        return self._get_bool('mqtt', 'enable', False)
    
    @property
    def mqtt_broker(self) -> str:
        if not self.config.has_section('mqtt'):
            return 'localhost'
        return self.config.get('mqtt', 'broker', fallback='localhost').strip()
    
    @property
    def mqtt_port(self) -> int:
        if not self.config.has_section('mqtt'):
            return 1883
        return self._get_int('mqtt', 'port', 1883)
    
    @property
    def mqtt_username(self) -> Optional[str]:
        if not self.config.has_section('mqtt'):
            return None
        value = self.config.get('mqtt', 'username', fallback='').strip()
        return value if value else None
    
    @property
    def mqtt_password(self) -> Optional[str]:
        if not self.config.has_section('mqtt'):
            return None
        value = self.config.get('mqtt', 'password', fallback='').strip()
        return value if value else None
    
    @property
    def mqtt_discovery_prefix(self) -> str:
        if not self.config.has_section('mqtt'):
            return 'homeassistant'
        return self.config.get('mqtt', 'discovery_prefix', fallback='homeassistant').strip()
    
    @property
    def mqtt_discovery_timeout(self) -> int:
        if not self.config.has_section('mqtt'):
            return 10
        return self._get_int('mqtt', 'discovery_timeout', 10)
    # Optionen
    @property
    def clear_tables(self) -> bool:
        return self._get_bool('options', 'clear_tables', True)
    
    @property
    def log_level(self) -> str:
        level = self.config.get('options', 'log_level', fallback='INFO').strip().upper()
        valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
        return level if level in valid_levels else 'INFO'
    
    @property
    def log_file(self) -> Optional[str]:
        value = self.config.get('options', 'log_file', fallback='').strip()
        return value if value else None
    
    # Advanced
    @property
    def scan_threads(self) -> int:
        if self.config.has_section('advanced'):
            return self._get_int('advanced', 'scan_threads', 50)
        return 50


# Konfiguration laden (global, wird in main() initialisiert)
config = None

# Logging konfigurieren (wird nach Config-Laden angepasst)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


class TahomaAPI:
    """Klasse für die Kommunikation mit der Tahoma Local API"""
    
    def __init__(self, gateway_ip: str, api_token: str):
        """
        Initialisiert die Tahoma API Verbindung
        
        Args:
            gateway_ip: IP-Adresse der Tahoma Box
            api_token: API Token (Bearer Token)
        """
        self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI"
        self.headers = {
            "Authorization": f"Bearer {api_token}",
            "Content-Type": "application/json"
        }
    
    def get_setup(self) -> Optional[Dict]:
        """
        Ruft die komplette Setup-Konfiguration ab
        
        Returns:
            Dictionary mit allen Geräten oder None bei Fehler
        """
        try:
            url = f"{self.base_url}/setup"
            response = requests.get(url, headers=self.headers, verify=False, timeout=10)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}")
            return None
    
    def get_devices(self) -> List[Dict]:
        """
        Extrahiert alle Geräte aus dem Setup
        
        Returns:
            Liste aller Geräte
        """
        setup = self.get_setup()
        if not setup:
            return []
        
        devices = setup.get('devices', [])
        logger.info(f"{len(devices)} Geräte gefunden")
        return devices
    
    def get_device_definition(self, device_url: str) -> Optional[Dict]:
        """
        Ruft die detaillierte Definition eines Geräts ab
        
        Args:
            device_url: URL des Geräts
            
        Returns:
            Dictionary mit Gerätedefinition oder None bei Fehler
        """
        try:
            # Device URL encodieren
            from urllib.parse import quote
            encoded_url = quote(device_url, safe='')
            url = f"{self.base_url}/setup/devices/{encoded_url}"
            response = requests.get(url, headers=self.headers, verify=False, timeout=10)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            logger.debug(f"Fehler beim Abrufen der Device-Definition für {device_url}: {e}")
            return None
    
    def get_device_states(self, device_url: str) -> List[Dict]:
        """
        Ruft die aktuellen States eines Geräts ab
        
        Args:
            device_url: URL des Geräts
            
        Returns:
            Liste der States
        """
        try:
            from urllib.parse import quote
            encoded_url = quote(device_url, safe='')
            url = f"{self.base_url}/setup/devices/{encoded_url}/states"
            response = requests.get(url, headers=self.headers, verify=False, timeout=10)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            logger.debug(f"Fehler beim Abrufen der Device-States für {device_url}: {e}")
            return []


class DatabaseManager:
    """Klasse für die MySQL-Datenbankoperationen"""
    
    def __init__(self, host: str, database: str, user: str, password: str, port: int = 3306):
        """
        Initialisiert die Datenbankverbindung
        
        Args:
            host: MySQL Host
            database: Datenbankname
            user: Benutzername
            password: Passwort
            port: Port (Standard: 3306)
        """
        self.host = host
        self.database = database
        self.user = user
        self.password = password
        self.port = port
        self.connection = None
    
    def connect(self) -> bool:
        """
        Stellt Verbindung zur Datenbank her
        
        Returns:
            True bei Erfolg, False bei Fehler
        """
        try:
            self.connection = pymysql.connect(
                host=self.host,
                database=self.database,
                user=self.user,
                password=self.password,
                port=self.port,
                charset='utf8mb4'
            )
            logger.info("Erfolgreich mit MariaDB/MySQL-Datenbank verbunden")
            return True
        except Error as e:
            logger.error(f"Fehler bei der Datenbankverbindung: {e}")
            return False
    
    def disconnect(self):
        """Schließt die Datenbankverbindung"""
        if self.connection:
            self.connection.close()
            logger.info("Datenbankverbindung geschlossen")
    
    def clear_tables(self):
        """Löscht alle Einträge aus allen Tabellen"""
        try:
            cursor = self.connection.cursor()
            # Foreign Key Constraints temporär deaktivieren
            cursor.execute("SET FOREIGN_KEY_CHECKS=0")
            
            # Alle Tabellen leeren
            cursor.execute("DELETE FROM command_parameters")
            cursor.execute("DELETE FROM actor_commands")
            cursor.execute("DELETE FROM actor_states")
            cursor.execute("DELETE FROM actors")
            
            cursor.execute("DELETE FROM sensor_states")
            cursor.execute("DELETE FROM sensors")
            
            # Foreign Key Constraints wieder aktivieren
            cursor.execute("SET FOREIGN_KEY_CHECKS=1")
            
            self.connection.commit()
            logger.info("Alle Tabellen geleert")
            cursor.close()
        except Error as e:
            logger.error(f"Fehler beim Leeren der Tabellen: {e}")
            self.connection.rollback()
    
    def insert_actor(self, device_type: str, name: str, url: str, 
                     commands: list, states: list) -> bool:
        """
        Fügt einen Aktor mit Commands und States in die Datenbank ein
        (AKTUALISIERT für MQTT URL-Support)
        """
        try:
            cursor = self.connection.cursor()
            
            # 1. Aktor einfügen
            query = """
                INSERT INTO actors (type, name, parameters, url)
                VALUES (%s, %s, NULL, %s)
            """
            cursor.execute(query, (device_type, name, url))
            actor_id = cursor.lastrowid
            
            # 2. Commands einfügen
            for cmd in commands:
                command_name = cmd.get('command', '')
                
                cmd_query = """
                    INSERT INTO actor_commands (actor_id, command_name)
                    VALUES (%s, %s)
                """
                cursor.execute(cmd_query, (actor_id, command_name))
                command_id = cursor.lastrowid
                
                # Parameter mit URL einfügen
                cmd_params = cmd.get('parameters', [])
                for param in cmd_params:
                    param_query = """
                        INSERT INTO command_parameters 
                        (command_id, parameter_name, parameter_type, min_value, max_value, possible_values, url)
                        VALUES (%s, %s, %s, %s, %s, %s, %s)
                    """
                    param_name = param.get('name', '')
                    param_type = param.get('type', '')
                    min_val = param.get('min')
                    max_val = param.get('max')
                    possible_vals = json.dumps(param.get('values')) if 'values' in param else None
                    param_url = param.get('url')  # NEU: MQTT Topic
                    
                    cursor.execute(param_query, 
                                 (command_id, param_name, param_type, min_val, max_val, possible_vals, param_url))
            
            # 3. States mit URL einfügen
            for state in states:
                state_query = """
                    INSERT INTO actor_states 
                    (actor_id, state_name, state_type, current_value, unit, url)
                    VALUES (%s, %s, %s, %s, %s, %s)
                """
                state_name = state.get('name', '')
                state_type = state.get('type', 0)
                current_value = str(state.get('current_value', '')) if 'current_value' in state else None
                unit = state.get('unit')
                state_url = state.get('url')  # NEU: MQTT Topic
                
                cursor.execute(state_query, (actor_id, state_name, state_type, current_value, unit, state_url))
            
            self.connection.commit()
            cursor.close()
            return True
            
        except Error as e:
            logger.error(f"Fehler beim Einfügen des Aktors {name}: {e}")
            self.connection.rollback()
            return False

    def insert_sensor(self, device_type: str, name: str, url: str, 
                      states: list) -> bool:
        """
        Fügt einen Sensor mit States in die Datenbank ein
        (AKTUALISIERT für MQTT URL-Support)
        """
        try:
            cursor = self.connection.cursor()
            
            # 1. Sensor einfügen
            query = """
                INSERT INTO sensors (type, name, parameters, url)
                VALUES (%s, %s, NULL, %s)
            """
            cursor.execute(query, (device_type, name, url))
            sensor_id = cursor.lastrowid
            
            # 2. States mit URL einfügen
            for state in states:
                state_query = """
                    INSERT INTO sensor_states 
                    (sensor_id, state_name, state_type, current_value, unit, url)
                    VALUES (%s, %s, %s, %s, %s, %s)
                """
                state_name = state.get('name', '')
                state_type = state.get('type', 0)
                current_value = str(state.get('current_value', '')) if 'current_value' in state else None
                unit = state.get('unit')
                state_url = state.get('url')  # NEU: MQTT Topic
                
                cursor.execute(state_query, (sensor_id, state_name, state_type, current_value, unit, state_url))
            
            self.connection.commit()
            cursor.close()
            return True
            
        except Error as e:
            logger.error(f"Fehler beim Einfügen des Sensors {name}: {e}")
            self.connection.rollback()
            return False


class WLEDDiscovery:
    """Klasse für die automatische WLED-Geräteerkennung im Netzwerk"""
    
    @staticmethod
    def discover_devices(timeout: int = 5) -> List[str]:
        """
        Sucht nach WLED-Geräten im lokalen Netzwerk mittels mDNS
        
        Args:
            timeout: Timeout in Sekunden für die Suche
            
        Returns:
            Liste mit IP-Adressen gefundener WLED-Geräte
        """
        try:
            from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
            import time
            
            class WLEDListener(ServiceListener):
                def __init__(self):
                    self.devices = []
                
                def add_service(self, zc, type_, name):
                    info = zc.get_service_info(type_, name)
                    if info:
                        # IP-Adresse extrahieren
                        addresses = [socket.inet_ntoa(addr) for addr in info.addresses]
                        for addr in addresses:
                            if addr not in self.devices:
                                self.devices.append(addr)
                                logger.info(f"WLED-Gerät gefunden: {name} ({addr})")
                
                def remove_service(self, zc, type_, name):
                    pass
                
                def update_service(self, zc, type_, name):
                    pass
            
            zeroconf = Zeroconf()
            listener = WLEDListener()
            browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)
            
            logger.info(f"Suche nach WLED-Geräten (Timeout: {timeout}s)...")
            time.sleep(timeout)
            
            zeroconf.close()
            
            # Filtern: Nur WLED-Geräte
            wled_devices = []
            for ip in listener.devices:
                if WLEDDiscovery.is_wled_device(ip):
                    wled_devices.append(ip)
            
            logger.info(f"{len(wled_devices)} WLED-Geräte gefunden")
            return wled_devices
            
        except ImportError:
            logger.warning("zeroconf-Bibliothek nicht installiert. Verwende Netzwerk-Scan...")
            return WLEDDiscovery.scan_network()
        except Exception as e:
            logger.error(f"Fehler bei WLED-Discovery: {e}")
            return []
    
    @staticmethod
    def scan_network(network: str = None, max_threads: int = 50) -> List[str]:
        """
        Scannt das Netzwerk nach WLED-Geräten (Fallback-Methode)
        
        Args:
            network: Netzwerk im Format "192.168.1.0/24" (None = automatisch)
            max_threads: Maximale Anzahl paralleler Threads
            
        Returns:
            Liste mit IP-Adressen gefundener WLED-Geräte
        """
        import socket
        import concurrent.futures
        
        if network is None:
            # Eigene IP ermitteln und Netzwerk ableiten
            try:
                s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                s.connect(("8.8.8.8", 80))
                local_ip = s.getsockname()[0]
                s.close()
                # Netzwerk ableiten (angenommen /24)
                network_prefix = '.'.join(local_ip.split('.')[:-1])
            except:
                logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x")
                network_prefix = "192.168.1"
        else:
            network_prefix = '.'.join(network.split('.')[:3])
        
        logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach WLED-Geräten...")
        
        def check_ip(ip):
            if WLEDDiscovery.is_wled_device(ip):
                return ip
            return None
        
        wled_devices = []
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
            futures = [executor.submit(check_ip, f"{network_prefix}.{i}") 
                      for i in range(1, 255)]
            
            for future in concurrent.futures.as_completed(futures):
                result = future.result()
                if result:
                    wled_devices.append(result)
                    logger.info(f"WLED-Gerät gefunden: {result}")
        
        return wled_devices
    
    @staticmethod
    def is_wled_device(ip: str, timeout: float = 1.0) -> bool:
        """
        Prüft ob eine IP-Adresse ein WLED-Gerät ist
        
        Args:
            ip: IP-Adresse
            timeout: Timeout für die Anfrage
            
        Returns:
            True wenn WLED-Gerät, False sonst
        """
        try:
            response = requests.get(
                f"http://{ip}/json/info", 
                timeout=timeout,
                headers={'User-Agent': 'TahomaSync/1.0'}
            )
            if response.status_code == 200:
                data = response.json()
                # WLED antwortet mit spezifischen Feldern
                return 'ver' in data or 'name' in data
        except:
            pass
        return False


class WLEDAPI:
    """Klasse für die Kommunikation mit WLED-Geräten"""
    
    def __init__(self, ip: str):
        """
        Initialisiert die WLED-API Verbindung
        
        Args:
            ip: IP-Adresse des WLED-Geräts
        """
        self.ip = ip
        self.base_url = f"http://{ip}"
    
    def get_info(self) -> Optional[Dict]:
        """
        Ruft Geräteinformationen ab
        
        Returns:
            Dictionary mit Geräteinformationen oder None
        """
        try:
            response = requests.get(f"{self.base_url}/json/info", timeout=2)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            logger.error(f"Fehler beim Abrufen der WLED-Info von {self.ip}: {e}")
            return None
        
    def get_presets(self) -> Optional[Dict]:
        """
        Ruft die Presets ab
        
        Returns:
            Dictionary mit Presets oder None
        """
        try:
            response = requests.get(f"{self.base_url}/presets.json", timeout=2)
            response.raise_for_status()
            presets_data = response.json()
            # Nur die relevanten Daten extrahieren
            preset_list = []
            if isinstance(presets_data, dict):
                for preset_id, preset_data in presets_data.items():
                    preset_name = preset_data.get('n', f'Preset {preset_id}')
                    preset_list.append({
                        int(preset_id): preset_name
                    })
            
            return preset_list
        except Exception as e:
            logger.error(f"Fehler beim Abrufen der WLED-Presets von {self.ip}: {e}")
            return None
        
    def get_effects(self) -> Optional[Dict]:
        """
        Ruft die Effects ab
        
        Returns:
            Dictionary mit effects oder None
        """
        try:
            response = requests.get(f"{self.base_url}/json/eff", timeout=2)
            response.raise_for_status()
            eff_data = response.json()
            # Nur die relevanten Daten extrahieren
            eff_list = []
            for eff_id, eff_data in enumerate(eff_data):
                if(eff_data):
                    eff_name = eff_data
                else:
                    eff_name = "Effect "+eff_id
                eff_list.append({
                    int(eff_id): eff_name
                })
            
            return eff_list
        except Exception as e:
            logger.error(f"Fehler beim Abrufen der WLED-Effects von {self.ip}: {e}")
            return None
        
    def get_state(self) -> Optional[Dict]:
        """
        Ruft aktuellen Zustand ab
        
        Returns:
            Dictionary mit aktuellem Zustand oder None
        """
        try:
            response = requests.get(f"{self.base_url}/json/state", timeout=2)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            logger.error(f"Fehler beim Abrufen des WLED-State von {self.ip}: {e}")
            return None
    
    def get_device_data(self) -> Optional[Dict]:
        """
        Erstellt ein Geräte-Dictionary im Tahoma-ähnlichen Format
        
        Returns:
            Dictionary mit Gerätedaten
        """
        info = self.get_info()
        state = self.get_state()
        
        if not info:
            return None
        
        # Name des Geräts
        name = info.get('name', f"WLED {self.ip}")
        preset_values = self.get_presets()
        eff_values = self.get_effects()
        # Commands für WLED erstellen
        commands = [
            {
                'command': 'on',
                'parameters': []
            },
            {
                'command': 'off',
                'parameters': []
            },
            {
                'command': 'setBrightness',
                'parameters': [
                    {
                        'name': 'brightness',
                        'type': 'integer',
                        'min': 0,
                        'max': 255
                    }
                ]
            },
            {
                'command': 'setColor',
                'parameters': [
                    {
                        'name': 'red',
                        'type': 'integer',
                        'min': 0,
                        'max': 255
                    },
                    {
                        'name': 'green',
                        'type': 'integer',
                        'min': 0,
                        'max': 255
                    },
                    {
                        'name': 'blue',
                        'type': 'integer',
                        'min': 0,
                        'max': 255
                    }
                ]
            },
            {
                'command': 'setEffect',
                'parameters': [
                    {
                        'name': 'effect',
                        'type': 'integer',
                        'min': 0,
                        'max': 255,
                        'values':eff_values
                    }
                ]
            },
            {
                'command': 'setPreset',
                'parameters': [
                    {
                        'name': 'preset',
                        'type': 'integer',
                        'min': 1,
                        'max': 250,
                        'values': preset_values
                    }
                ]
            }
        ]
        
        # States erstellen
        states = []
        if state:
            states.append({
                'name': 'power',
                'type': 'boolean',
                'current_value': state.get('on', False)
            })
            states.append({
                'name': 'brightness',
                'type': 'integer',
                'current_value': state.get('bri', 0)
            })
            
            # Farbe (erstes Segment)
            segments = state.get('seg', [])
            if segments and len(segments) > 0:
                seg = segments[0]
                colors = seg.get('col', [[0,0,0]])
                if colors and len(colors) > 0:
                    rgb = colors[0]
                    states.append({
                        'name': 'color_rgb',
                        'type': 'array',
                        'current_value': rgb
                    })
        
        return {
            'name': name,
            'type': 'WLED',
            'url': f"wled://{self.ip}",
            'commands': commands,
            'states': states,
            'info': {
                'version': info.get('ver', 'unknown'),
                'ip': self.ip,
                'mac': info.get('mac', 'unknown')
            }
        }


class DeviceClassifier:
    """Klassifiziert Geräte als Aktoren oder Sensoren"""
    
    # Bekannte Aktor-Typen (können erweitert werden)
    ACTOR_TYPES = {
        'RollerShutter', 'ExteriorScreen', 'Awning', 'Blind', 
        'GarageDoor', 'Window', 'Light', 'OnOff', 'DimmableLight',
        'HeatingSystem', 'Valve', 'Switch', 'Door', 'Curtain',
        'VenetianBlind', 'PergolaScreen'
    }
    
    # Bekannte Sensor-Typen (können erweitert werden)
    SENSOR_TYPES = {
        'TemperatureSensor', 'LightSensor', 'HumiditySensor',
        'ContactSensor', 'OccupancySensor', 'SmokeSensor',
        'WaterDetectionSensor', 'WindowHandle', 'MotionSensor',
        'SunSensor', 'WindSensor', 'RainSensor', 'ConsumptionSensor'
    }

    # Bekannte Kommandos (können erweitert werden)
    TAHOMA_COMMANDS = {
        # ---------- Jalousien & Rollos ----------
        "setClosure": [
            {
                "name": "position",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "0 % = offen, 100 % = zu"
            }
        ],
        "setClosureAndOrientation": [
            {
                "name": "position",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "0 % = offen, 100 % = zu"
            },
            {
                "name": "neigung",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "0 % = offen, 100 % = zu (Neigungswinkel bei Lamellen)"
            },
        ],
        "setOrientation": [
            {
                "name": "neigung",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "0 % = offen, 100 % = zu (Nur Neigungswinkel)"
            }
        ],
        "up": [],                     # öffnet vollständig → kein Parameter
        "down": [],                   # schließt vollständig → kein Parameter
        "my": [],                     # fährt zur „Meine‑Position“
        "setMyPosition": [
            {
                "name": "position",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "Speichert die aktuelle bzw. angegebene Position (0 % = offen, 100 % = zu)"
            }
        ],
        "stop": [],                   # sofort anhalten
        "refresh": [],                # Status neu abfragen

        # ---------- Licht / Schalter ----------
        "on": [],
        "off": [],
        "toggle": [],
        "setIntensity": [
            {
                "name": "helligkeit",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "Helligkeit des Lichts"
            }
        ],
        "setColor": [
            {
                "name": "farbton",
                "type": "integer",
                "min": 0,
                "max": 360,
                "unit": "°",
                "description": "Farbton (0‑360°)"
            },
            {
                "name": "sättigung",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "Sättigung des Farbtons"
            },
        ],
        "setColorTemperature": [
            {
                "name": "farbtemperatur",
                "type": "integer",
                "min": 2000,
                "max": 6500,
                "unit": "K",
                "description": "Farbtemperatur in Kelvin"
            }
        ],
        "setTransition": [
            {
                "name": "dauer",
                "type": "integer",
                "min": 0,
                "max": 3600,
                "unit": "s",
                "description": "Übergangszeit für nachfolgende Befehle"
            }
        ],

        # ---------- Thermostat ----------
        "setTargetTemperature": [
            {
                "name": "temperatur",
                "type": "float",
                "min": 5.0,
                "max": 30.0,
                "unit": "°C",
                "description": "Soll‑Temperatur"
            }
        ],
        "setMode": [
            {
                "name": "betriebsart",
                "type": "string",
                "enum": ["off", "heating", "cooling", "auto"],
                "unit": None,
                "description": "Betriebsmodus des Thermostats"
            }
        ],
        "setBoost": [
            {
                "name": "boost_dauer",
                "type": "integer",
                "min": 1,
                "max": 180,
                "unit": "min",
                "description": "Kurz‑Boost‑Dauer"
            }
        ],

        # ---------- Schalt‑ / Szenen‑Geräte ----------
        "pulse": [
            {
                "name": "impuls_dauer",
                "type": "integer",
                "min": 1,
                "max": 3600,
                "unit": "s",
                "description": "Kurzimpuls‑Dauer"
            }
        ],
        "setLevel": [
            {
                "name": "ausgangs_level",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "Ausgangs‑Level (dimmbare Relais)"
            }
        ],
        "trigger": [],                # Szene ausführen → kein Parameter

        # ---------- Meta‑Befehle (für alle Geräte) ----------
        "configureReporting": [
            {
                "name": "intervall",
                "type": "integer",
                "min": 30,
                "max": 86400,
                "unit": "s",
                "description": "Meldeintervall für das Gerät"
            }
        ],
        "setBatteryThreshold": [
            {
                "name": "warnschwelle",
                "type": "integer",
                "min": 0,
                "max": 100,
                "unit": "%",
                "description": "Batteriewarnschwelle"
            }
        ],
    }

        
   
    @classmethod
    def is_actor(cls, device: Dict) -> bool:
        """
        Prüft, ob ein Gerät ein Aktor ist
        
        Args:
            device: Geräte-Dictionary
            
        Returns:
            True wenn Aktor, False sonst
        """
        device_type = device.get('definition', '').get('uiClass', '')
        
        # Prüfung nach bekannten Typen
        if device_type in cls.ACTOR_TYPES:
            return True

        # Prüfung nach Commandos (Aktoren haben typischerweise Commands)
        commands = device.get('definition', {}).get('commands', [])
        if commands and len(commands) > 0:
            # Wenn Commands wie open, close, on, off existieren
            command_names = [cmd.get('commandName', '') for cmd in commands]
            actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'}
            if any(cmd in actor_commands for cmd in command_names):
                return True
        
        return False
    
    @classmethod
    def is_sensor(cls, device: Dict) -> bool:
        """
        Prüft, ob ein Gerät ein Sensor ist
        
        Args:
            device: Geräte-Dictionary
            
        Returns:
            True wenn Sensor, False sonst
        """
        device_type = device.get('definition', '').get('uiClass', '')
        controllable_name = device.get('controllableName', '')
        
        # Prüfung nach bekannten Typen
        if device_type in cls.SENSOR_TYPES:
            return True
        if controllable_name in cls.SENSOR_TYPES:
            return True
        else:
            return False
        # Prüfung nach States (Sensoren haben typischerweise nur States, keine Commands)
        states = device.get('states', [])
        commands = device.get('definition', {}).get('commands', [])
        
        # Sensor hat States aber keine oder nur wenige Commands
        if states and len(states) > 0 and len(commands) <= 1:
            return True
        
        return False

    @classmethod
    def extract_actor_data(cls, device: Dict) -> tuple:
        """
        Extrahiert Commands und States aus einem Aktor
        
        Args:
            device: Geräte-Dictionary von der Tahoma API
            
        Returns:
            Tuple (commands_list, states_list)
        """
        commands = []
        states = []
        
        # Commands aus der Definition extrahieren
        cmd_definitions = device.get('definition', {}).get('commands', [])
        
        for cmd in cmd_definitions:
            command_name = cmd.get('commandName', '')
            
            command_entry = {
                'command': command_name,
                'parameters': []
            }
            
            # HINWEIS: Tahoma API liefert oft keine detaillierten Parameter-Infos
            # Daher werden Commands erstmal ohne Parameter-Details gespeichert
            #cmd_params = cmd.get('parameters', [])
            cmd_params = cls.TAHOMA_COMMANDS.get(command_name,[])

            for cmd_param in cmd_params:
                param_detail = {
                    'name': cmd_param.get('name', '')
                }
                
                # Datentyp (falls vorhanden)
                param_type = cmd_param.get('type')
                if param_type:
                    param_detail['type'] = param_type
                
                # Min/Max Werte (meist nicht in Tahoma API vorhanden)
                if 'min' in cmd_param:
                    param_detail['min'] = cmd_param['min']
                if 'max' in cmd_param:
                    param_detail['max'] = cmd_param['max']
                
                # Mögliche Werte (enum) (meist nicht vorhanden)
                if 'values' in cmd_param:
                    param_detail['values'] = cmd_param['values']
                if 'unit' in cmd_param:
                    param_detail['unit'] = cmd_param['unit']
                if 'description' in cmd_param:
                    param_detail['description'] = cmd_param['description']    
                # Nur hinzufügen wenn Name vorhanden
                if param_detail['name']:
                    command_entry['parameters'].append(param_detail)
            
            commands.append(command_entry)
        
        # States extrahieren
        state_definitions = device.get('states', [])
        for state in state_definitions:
            state_name = state.get('name', '')
            if state_name:
                state_entry = {
                    'name': state_name,
                    'type': state.get('type', 0)
                }
                if 'value' in state:
                    state_entry['current_value'] = state['value']
                states.append(state_entry)
        
        return commands, states

    @classmethod
    def extract_sensor_data(cls, device: Dict) -> list:
        """
        Extrahiert States aus einem Sensor
        
        Args:
            device: Geräte-Dictionary von der Tahoma API
            
        Returns:
            Liste der States
        """
        states = []
        
        # States extrahieren
        state_definitions = device.get('states', [])
        for state in state_definitions:
            state_name = state.get('name', '')
            if state_name:
                state_entry = {
                    'name': state_name,
                    'type': state.get('type', 0)
                }
                
                # Aktueller Wert falls vorhanden
                if 'value' in state:
                    state_entry['current_value'] = state['value']
                
                states.append(state_entry)
        
        return states


def process_devices(tahoma: TahomaAPI, db: DatabaseManager, 
                    enable_wled: bool = True, wled_timeout: int = 5,
                    enable_mqtt: bool = True,  # NEU
                    clear_before_insert: bool = True):
    """
    Verarbeitet alle Geräte und speichert sie in der Datenbank
    
    Args:
        tahoma: TahomaAPI Instanz
        db: DatabaseManager Instanz
        enable_wled: WLED-Geräte suchen und hinzufügen
        wled_timeout: Timeout für WLED-Discovery in Sekunden
        clear_before_insert: Tabellen vor dem Einfügen leeren (Standard: True)
    """
    # Optional: Tabellen leeren
    if clear_before_insert:
        db.clear_tables()
    
    actor_count = 0
    sensor_count = 0
    unknown_count = 0
    
    # ========== TAHOMA-GERÄTE VERARBEITEN ==========
    logger.info("=" * 60)
    logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN")
    logger.info("=" * 60)
    
    devices = tahoma.get_devices()
    
    if not devices:
        logger.warning("Keine Tahoma-Geräte gefunden")
    else:
        # Gruppierte Geräte identifizieren (#1, #2, etc.)
        device_groups = {}  # {base_url: [devices]}
        standalone_devices = []
        
        for device in devices:
            device_url = device.get('deviceURL', '')
            
            # Prüfen ob URL mit #1, #2, etc. endet
            import re
            match = re.match(r'(.+)#(\d+)$', device_url)
            
            if match:
                base_url = match.group(1)
                if base_url not in device_groups:
                    device_groups[base_url] = []
                device_groups[base_url].append(device)
            else:
                standalone_devices.append(device)
        
        # Gruppierte Geräte verarbeiten
        for base_url, group_devices in device_groups.items():
            # Hauptgerät finden (ohne #-Endung oder mit #1)
            main_device = None
            sub_devices = []
            
            for dev in group_devices:
                url = dev.get('deviceURL', '')
                if url.endswith('#1'):
                    main_device = dev
                else:
                    sub_devices.append(dev)
            
            # Falls kein #1, nehme das erste Gerät als Hauptgerät
            if not main_device and group_devices:
                main_device = group_devices[0]
                sub_devices = group_devices[1:]
            
            # Name vom Hauptgerät für alle übernehmen
            main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt'
            
            # Alle Geräte der Gruppe verarbeiten mit gemeinsamem Namen
            for device in group_devices:
                device_url = device.get('deviceURL', '')
                # controllableName als Typ verwenden
                device_type = device.get('controllableName', device.get('uiClass', 'Unknown'))
                
                # Gerät klassifizieren
                is_actor = DeviceClassifier.is_actor(device)
                is_sensor = DeviceClassifier.is_sensor(device)
                
                if is_actor:
                    commands, states = DeviceClassifier.extract_actor_data(device)
                    if db.insert_actor(device_type, main_name, device_url, commands, states):
                        actor_count += 1
                        logger.info(f"✓ Aktor [Gruppe]: {main_name} ({device_type}) - "
                                  f"{len(commands)} Commands, {len(states)} States")
                
                elif is_sensor:
                    states = DeviceClassifier.extract_sensor_data(device)
                    if db.insert_sensor(device_type, main_name, device_url, states):
                        sensor_count += 1
                        logger.info(f"✓ Sensor [Gruppe]: {main_name} ({device_type}) - "
                                  f"{len(states)} States")
                else:
                    unknown_count += 1
                    logger.warning(f"⚠ Unbekannt [Gruppe]: {main_name} ({device_type}-{device_url})")
        
        # Standalone-Geräte verarbeiten
        for device in standalone_devices:
            device_url = device.get('deviceURL', '')
            device_name = device.get('label', 'Unbekannt')
            # controllableName als Typ verwenden
            device_type = device.get('controllableName', device.get('uiClass', 'Unknown'))
            
            # Gerät klassifizieren
            is_actor = DeviceClassifier.is_actor(device)
            is_sensor = DeviceClassifier.is_sensor(device)
            
            if is_actor:
                # Daten extrahieren
                commands, states = DeviceClassifier.extract_actor_data(device)
                
                # In Datenbank speichern
                if db.insert_actor(device_type, device_name, device_url, commands, states):
                    actor_count += 1
                    logger.info(f"✓ Aktor: {device_name} ({device_type}) - "
                              f"{len(commands)} Commands, {len(states)} States")
            
            elif is_sensor:
                # Daten extrahieren
                states = DeviceClassifier.extract_sensor_data(device)
                
                # In Datenbank speichern
                if db.insert_sensor(device_type, device_name, device_url, states):
                    sensor_count += 1
                    logger.info(f"✓ Sensor: {device_name} ({device_type}) - "
                              f"{len(states)} States")
            
            else:
                unknown_count += 1
                logger.warning(f"⚠ Unbekannt: {device_name} ({device_type})")
    
    # ========== WLED-GERÄTE SUCHEN UND VERARBEITEN ==========
    if enable_wled:
        logger.info("\n" + "=" * 60)
        logger.info("WLED-GERÄTE WERDEN GESUCHT")
        logger.info("=" * 60)
        
        wled_ips = WLEDDiscovery.discover_devices(timeout=wled_timeout)
        
        # Manuelle IPs aus Config hinzufügen
        if config and config.wled_manual_ips:
            logger.info(f"Füge {len(config.wled_manual_ips)} manuelle WLED-IPs hinzu...")
            for manual_ip in config.wled_manual_ips:
                if manual_ip not in wled_ips:
                    if WLEDDiscovery.is_wled_device(manual_ip):
                        wled_ips.append(manual_ip)
                        logger.info(f"✓ Manuelles WLED-Gerät: {manual_ip}")
                    else:
                        logger.warning(f"⚠ {manual_ip} ist kein WLED-Gerät")
        
        if not wled_ips:
            logger.info("Keine WLED-Geräte gefunden")
        else:
            logger.info(f"\n{len(wled_ips)} WLED-Geräte gefunden, füge sie hinzu...")
            
            for ip in wled_ips:
                try:
                    wled = WLEDAPI(ip)
                    device_data = wled.get_device_data()
                    
                    if device_data:
                        name = device_data['name']
                        device_type = device_data['type']
                        url = device_data['url']
                        commands = device_data['commands']
                        states = device_data['states']
                        
                        # WLED immer als Aktor hinzufügen
                        if db.insert_actor(device_type, name, url, commands, states):
                            actor_count += 1
                            logger.info(f"✓ WLED: {name} ({ip}) - "
                                      f"{len(commands)} Commands, {len(states)} States")
                    else:
                        logger.warning(f"⚠ Konnte keine Daten von WLED {ip} abrufen")
                        
                except Exception as e:
                    logger.error(f"✗ Fehler beim Verarbeiten von WLED {ip}: {e}")
    # ========== MQTT-GERÄTE SUCHEN UND VERARBEITEN ==========
    # ========== MQTT-GERÄTE SUCHEN UND VERARBEITEN ==========
    if enable_mqtt:
        logger.info("\n" + "=" * 60)
        logger.info("MQTT/HOME ASSISTANT DISCOVERY")
        logger.info("=" * 60)
        
        try:
            # MQTT Discovery initialisieren
            mqtt_discovery = HomeAssistantDiscovery(
                broker=config.mqtt_broker,
                port=config.mqtt_port,
                username=config.mqtt_username,
                password=config.mqtt_password,
                discovery_prefix=config.mqtt_discovery_prefix
            )
            
            # Verbinden
            if mqtt_discovery.connect():
                # Discovery durchführen
                mqtt_entities = mqtt_discovery.discover_devices(
                    timeout=config.mqtt_discovery_timeout
                )
                
                # Entities nach Geräten gruppieren
                mqtt_devices = MQTTDeviceConverter.group_entities_by_device(mqtt_entities)
                
                # Geräte verarbeiten
                if not mqtt_devices:
                    logger.info("Keine MQTT-Geräte gefunden")
                else:
                    logger.info(f"\n{len(mqtt_devices)} MQTT-Geräte gefunden (aus {len(mqtt_entities)} Entities)")
                    
                    for device_id, device_data in mqtt_devices.items():
                        try:
                            # Gerät in Actor/Sensor konvertieren
                            actor_data, sensor_data = MQTTDeviceConverter.convert_device_to_actors_and_sensors(
                                device_id, device_data
                            )
                            
                            # Actor speichern falls vorhanden
                            if actor_data:
                                if db.insert_actor(
                                    actor_data['type'],
                                    actor_data['name'],
                                    actor_data['url'],
                                    actor_data['commands'],
                                    actor_data['states']
                                ):
                                    actor_count += 1
                                    logger.info(f"✓ MQTT Actor: {actor_data['name']} - "
                                              f"{len(actor_data['commands'])} Commands, "
                                              f"{len(actor_data['states'])} States")
                            
                            # Sensor speichern falls vorhanden
                            if sensor_data:
                                if db.insert_sensor(
                                    sensor_data['type'],
                                    sensor_data['name'],
                                    sensor_data['url'],
                                    sensor_data['states']
                                ):
                                    sensor_count += 1
                                    logger.info(f"✓ MQTT Sensor: {sensor_data['name']} - "
                                              f"{len(sensor_data['states'])} States")
                                    
                        except Exception as e:
                            logger.error(f"✗ Fehler beim Verarbeiten von MQTT-Gerät {device_id}: {e}")
                            import traceback
                            traceback.print_exc()
                
                # Verbindung trennen
                mqtt_discovery.disconnect()
            else:
                logger.error("MQTT-Verbindung fehlgeschlagen")
                
        except Exception as e:
            logger.error(f"✗ MQTT Discovery Fehler: {e}")
            import traceback
            traceback.print_exc()

            
    # ========== ZUSAMMENFASSUNG ==========
    logger.info("\n" + "=" * 60)
    logger.info("ZUSAMMENFASSUNG")
    logger.info("=" * 60)
    logger.info(f"Aktoren gespeichert:   {actor_count}")
    logger.info(f"Sensoren gespeichert:  {sensor_count}")
    logger.info(f"Unbekannte Geräte:     {unknown_count}")
    logger.info("=" * 60)


def main():
    """Hauptfunktion"""
    global config
    
    # Konfigurationsdatei laden
    try:
        config = Config('config.ini')
        logger.info("Konfiguration erfolgreich geladen")
    except FileNotFoundError as e:
        print(f"FEHLER: {e}")
        print("\nBitte erstellen Sie eine config.ini Datei mit Ihren Einstellungen.")
        print("Siehe config.ini Vorlage für Details.")
        return
    except Exception as e:
        print(f"FEHLER beim Laden der Konfiguration: {e}")
        return
    
    # Logging-Level anpassen
    log_level = getattr(logging, config.log_level)
    logger.setLevel(log_level)
    
    # Optional: Log-Datei einrichten
    if config.log_file:
        file_handler = logging.FileHandler(config.log_file, encoding='utf-8')
        file_handler.setLevel(log_level)
        file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
        logger.addHandler(file_handler)
        logger.info(f"Logging in Datei: {config.log_file}")
    
    # Tahoma API initialisieren
    logger.info("Verbinde mit Tahoma Box...")
    tahoma = TahomaAPI(config.tahoma_ip, config.tahoma_token)
    
    # Datenbank initialisieren
    logger.info("Verbinde mit MySQL-Datenbank...")
    db = DatabaseManager(
        config.db_host, 
        config.db_name, 
        config.db_user, 
        config.db_password, 
        config.db_port
    )
    
    if not db.connect():
        logger.error("Datenbankverbindung fehlgeschlagen. Abbruch.")
        return
    
    try:
        # Geräte verarbeiten und in Datenbank speichern
        process_devices(
            tahoma, 
            db, 
            enable_wled=config.wled_enable,
            wled_timeout=config.wled_discovery_timeout,
            enable_mqtt=config.mqtt_enable,  # NEU
            clear_before_insert=config.clear_tables
        )
        logger.info("\n✓ Import erfolgreich abgeschlossen!")
        
    except Exception as e:
        logger.error(f"✗ Fehler während der Verarbeitung: {e}")
        import traceback
        traceback.print_exc()
    
    finally:
        # Datenbankverbindung schließen
        db.disconnect()


if __name__ == "__main__":
    main()