"""
MODULE SERVEUR BACNET/IP - NIVEAU INDUSTRIEL
Version Spécification: 1.2
Auteur: Assistant (Généré pour intégration industrielle)

Ce module implémente un serveur BACnet/IP robuste utilisant bacpypes3.
Il est conçu pour tourner dans un thread dédié au sein d'une application critique.

Nouveautés V1.2 + Patch:
- Ajout de set_description() pour mise à jour dynamique.
- Ajout de set_state_texts() pour mise à jour dynamique des libellés (BV/MSV).
- Ajout de get_value() pour lecture simple (Polling).
- Ajout de get_priority_array() pour diagnostic avancé.
- Correction Import bacpypes3 (DeviceObject) et init Application.
- Suppression imports inutiles (ReadableProperty, WritableProperty).
- Correction attribut 'localDevice' -> 'device' pour compatibilité bacpypes3.
- PATCH: Stockage explicite de self.device dans BACnetServerEngine pour éviter AttributeError.
- PATCH: Nettoyage propre des tâches asyncio à l'arrêt pour éviter "Task was destroyed but it is pending".
"""

import asyncio
import threading
import time
import logging
import gc
import weakref
from typing import Dict, List, Optional, Any, Union, Tuple, Callable

# Import bacpypes3
try:
    from bacpypes3.settings import settings
    from bacpypes3.debugging import bacpypes_debugging, ModuleLogger
    from bacpypes3.argparse import SimpleArgumentParser
    from bacpypes3.app import Application
    # LocalDeviceObject n'existe pas dans bacpypes3.local.device, on utilise DeviceObject
    from bacpypes3.primitivedata import ObjectIdentifier, CharacterString, Unsigned
    from bacpypes3.basetypes import (
        EngineeringUnits, PropertyIdentifier, Reliability, StatusFlags,
        EventState, DeviceStatus, ServicesSupported, ObjectTypesSupported
    )
    # Correction: Suppression de ReadableProperty et WritableProperty qui n'existent pas/plus
    from bacpypes3.object import (
        AnalogValueObject, BinaryValueObject, MultiStateValueObject,
        DeviceObject
    )
except ImportError:
    raise ImportError("La librairie 'bacpypes3' est requise. Install via: pip install bacpypes3")

# --- CONSTANTES & CONFIGURATION ---

SPEC_VERSION = "1.2"
DEFAULT_PORT = 47808
DEFAULT_VENDOR_ID = 555

RELIABILITY_NO_SENSOR = Reliability.noSensor
RELIABILITY_NO_FAULT = Reliability.noFaultDetected
RELIABILITY_PROCESS_ERROR = Reliability.processError

# --- SECTION 1: HELPER CLASSES ---

class UnitMapper:
    """Convertit les unités textuelles de l'application en énumérations BACnet."""
    def __init__(self, mapping: Dict[str, int]):
        self._map = mapping

    def get_bacnet_unit(self, unit_str: Optional[str]) -> EngineeringUnits:
        if not unit_str:
            return EngineeringUnits.noUnits
        unit_code = self._map.get(unit_str)
        if unit_code is not None:
            try:
                return EngineeringUnits(unit_code)
            except ValueError:
                pass
        return EngineeringUnits.noUnits

# --- SECTION 2: CUSTOM BACNET OBJECTS ---

class NotifyMixin:
    """Mixin pour notifier l'interface lors d'une écriture externe."""
    _write_callback: Optional[Callable[[str, Any, int], None]] = None

    def set_write_callback(self, cb):
        self._write_callback = cb

    async def write_property(self, attr: str, value: Any, priority: Optional[int] = None):
        await super().write_property(attr, value, priority)
        if attr == "presentValue" and priority is not None and self._write_callback:
            self._write_callback(self.objectName, value, priority)

class CustomAnalogValue(AnalogValueObject): pass
class WritableAnalogValue(NotifyMixin, AnalogValueObject): pass

class CustomBinaryValue(BinaryValueObject): pass
class WritableBinaryValue(NotifyMixin, BinaryValueObject): pass

class CustomMultiStateValue(MultiStateValueObject): pass
class WritableMultiStateValue(NotifyMixin, MultiStateValueObject): pass


# --- SECTION 3: ENGINE ---

class BACnetServerEngine(Application):
    """La stack BACnet proprement dite."""
    def __init__(self, device_info: DeviceObject, address: str, interface_ref):
        # bacpypes3 Application prend (device, address)
        super().__init__(device_info, address)
        self.interface_ref = weakref.ref(interface_ref)
        self.log = interface_ref.logger
        self._objects_map = {}

        # PATCH ROBUSTESSE: Stockage explicite de la référence device
        # Cela garantit que self.device existe même si la classe parente Application change d'implémentation
        self.device = device_info

    def add_app_object(self, obj):
        self.add_object(obj)
        self._objects_map[obj.objectName] = obj

    def get_app_object(self, name: str):
        return self._objects_map.get(name)

    def set_system_status(self, status: DeviceStatus):
        # Utilise l'attribut explicitement stocké
        self.device.systemStatus = status


# --- SECTION 4: SERVICE (THREAD WRAPPER) ---

class BACnetService(threading.Thread):
    """Thread dédié BACnet (Instance à usage unique)."""
    def __init__(self,
                 interface,
                 device_id: int,
                 ip_address: str,
                 port: int,
                 vendor_id: int,
                 vendor_name: str,
                 model_name: str,
                 config_objects: List[dict],
                 app_watchdog_timeout: int):

        super().__init__(name="BACnetServiceThread", daemon=True)
        self.interface = interface
        self.logger = interface.logger

        self.device_id = device_id
        self.ip_address = ip_address
        self.port = port
        self.vendor_id = vendor_id
        self.vendor_name = vendor_name
        self.model_name = model_name
        self.config_objects = config_objects
        self.app_watchdog_timeout = app_watchdog_timeout

        self.loop = None
        self.engine: Optional[BACnetServerEngine] = None
        self.ready_event = threading.Event()
        self.stop_event = asyncio.Event()
        self.fatal_error: Optional[Exception] = None
        self.last_alive_timestamp = 0.0
        self.last_app_feed_timestamp = time.time()

    def run(self):
        self.last_alive_timestamp = time.time()
        try:
            self.loop = asyncio.new_event_loop()
            asyncio.set_event_loop(self.loop)
            self.loop.run_until_complete(self._main_task())
        except Exception as e:
            self.fatal_error = e
            self.logger.critical(f"[BACnet] Fatal Thread Crash: {e}", exc_info=True)
        finally:
            if self.loop:
                self.loop.close()
            self.logger.info("[BACnet] Thread stopped.")

    async def _main_task(self):
        tasks = [] # Initialisation pour le finally
        try:
            self.logger.info(f"[BACnet] Initializing Device {self.device_id}...")
            # Utilisation de DeviceObject
            local_device = DeviceObject(
                objectName=self.model_name,
                objectIdentifier=ObjectIdentifier(f"device,{self.device_id}"),
                maxApduLengthAccepted=1476,
                segmentationSupported="segmentedBoth",
                vendorIdentifier=self.vendor_id,
                vendorName=self.vendor_name,
                modelName=self.model_name,
                systemStatus=DeviceStatus.operational
            )

            address = f"{self.ip_address}:{self.port}"
            # Passage de l'adresse au constructeur
            self.engine = BACnetServerEngine(local_device, address, self.interface)
            self._instantiate_objects()

            persistence_data = self.interface.on_persistence_load()
            if persistence_data:
                self._apply_persistence(persistence_data)

            self.ready_event.set()
            self.logger.info(f"[BACnet] Server Ready ({address})")

            tasks = [
                asyncio.create_task(self._cpu_yield_task()),
                asyncio.create_task(self._buffer_consumer_task()),
                asyncio.create_task(self._watchdog_monitor_task()),
                asyncio.create_task(self.stop_event.wait())
            ]

            # Attend que l'une des tâches finisse (normalement stop_event.wait())
            await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)

        except Exception as e:
            self.fatal_error = e
            self.ready_event.set()
            raise e
        finally:
            # PATCH: Annulation propre des tâches de fond
            for task in tasks:
                if not task.done():
                    task.cancel()

            # Attendre que l'annulation soit effective pour éviter les logs d'erreur
            if tasks:
                await asyncio.gather(*tasks, return_exceptions=True)

            if self.engine:
                self.engine.close()

    def _instantiate_objects(self):
        mapper = self.interface.unit_mapper
        for cfg in self.config_objects:
            name = cfg['name']
            obj_type = cfg['type']
            writable = cfg['writable']
            desc = cfg.get('description', '')
            unit_enum = mapper.get_bacnet_unit(cfg.get('units'))
            initial = cfg.get('initial_value')

            new_obj = None
            base_kwargs = {
                'objectName': name,
                'description': desc,
                'statusFlags': [0, 0, 0, 0],
                'eventState': EventState.normal,
                'reliability': Reliability.noFaultDetected,
                'outOfService': False
            }

            if obj_type == 'AV':
                cls = WritableAnalogValue if writable else CustomAnalogValue
                base_kwargs.update({
                    'objectIdentifier': ObjectIdentifier(f"analogValue,{self._get_next_instance_id('AV')}"),
                    'units': unit_enum,
                    'presentValue': initial if initial is not None else 0.0,
                })
                new_obj = cls(**base_kwargs)

            elif obj_type == 'BV':
                cls = WritableBinaryValue if writable else CustomBinaryValue
                base_kwargs.update({
                    'objectIdentifier': ObjectIdentifier(f"binaryValue,{self._get_next_instance_id('BV')}"),
                    'activeText': cfg.get('active_text', 'On'),
                    'inactiveText': cfg.get('inactive_text', 'Off'),
                    'presentValue': initial if initial is not None else 0,
                })
                new_obj = cls(**base_kwargs)

            elif obj_type == 'MSV':
                cls = WritableMultiStateValue if writable else CustomMultiStateValue
                states = cfg.get('state_texts', ['State1'])
                base_kwargs.update({
                    'objectIdentifier': ObjectIdentifier(f"multiStateValue,{self._get_next_instance_id('MSV')}"),
                    'numberOfStates': len(states),
                    'stateText': [CharacterString(s) for s in states],
                    'presentValue': initial if initial is not None else 1,
                })
                new_obj = cls(**base_kwargs)

            if new_obj:
                if writable and isinstance(new_obj, NotifyMixin):
                    new_obj.set_write_callback(self._on_external_write_callback)
                self.engine.add_app_object(new_obj)

    def _get_next_instance_id(self, type_prefix):
        if not hasattr(self, '_id_counters'): self._id_counters = {}
        idx = self._id_counters.get(type_prefix, 0)
        self._id_counters[type_prefix] = idx + 1
        return idx

    def _apply_persistence(self, data: dict):
        pass # TODO: Implémentation fine selon format

    def _on_external_write_callback(self, name, value, priority):
        try:
            self.interface.on_external_write(name, value, priority)
        except Exception as e:
            self.logger.error(f"[BACnet] Error in external write hook: {e}")

    async def _cpu_yield_task(self):
        while not self.stop_event.is_set():
            self.last_alive_timestamp = time.time()
            try:
                await asyncio.sleep(0.01)
                time.sleep(0)
            except asyncio.CancelledError:
                break

    async def _buffer_consumer_task(self):
        while not self.stop_event.is_set():
            try:
                updates = self.interface.pop_pending_updates()
                if updates:
                    for name, data in updates.items():
                        obj = self.engine.get_app_object(name)
                        if not obj: continue

                        value, is_error, is_manual, is_alarm = data

                        flags = [0, 0, 0, 0]
                        if is_error: flags[1] = 1
                        if is_manual: flags[2] = 1
                        if is_alarm: flags[0] = 1

                        obj.statusFlags = flags
                        obj.reliability = RELIABILITY_NO_SENSOR if is_error else RELIABILITY_NO_FAULT
                        obj.eventState = EventState.offnormal if is_alarm else EventState.normal

                        if value is not None:
                            if isinstance(obj, (WritableAnalogValue, WritableBinaryValue, WritableMultiStateValue)):
                                obj.relinquishDefault = value
                            else:
                                obj.presentValue = value
                await asyncio.sleep(0.05)
            except asyncio.CancelledError:
                break
            except Exception as e:
                self.logger.error(f"[BACnet] Buffer Error: {e}")
                await asyncio.sleep(1)

    async def _watchdog_monitor_task(self):
        if self.app_watchdog_timeout <= 0: return
        while not self.stop_event.is_set():
            try:
                elapsed = time.time() - self.last_app_feed_timestamp
                if elapsed > self.app_watchdog_timeout:
                    # Utilise self.engine.device maintenant garanti
                    if self.engine.device.systemStatus != DeviceStatus.nonOperational:
                        self.logger.warning(f"[BACnet] DEAD MAN SWITCH! App frozen {elapsed:.1f}s.")
                        self.engine.set_system_status(DeviceStatus.nonOperational)
                else:
                    if self.engine.device.systemStatus == DeviceStatus.nonOperational:
                        self.logger.info("[BACnet] App alive. System Operational.")
                        self.engine.set_system_status(DeviceStatus.operational)
                await asyncio.sleep(1)
            except asyncio.CancelledError:
                break

    def stop(self):
        if self.loop: self.loop.call_soon_threadsafe(self.stop_event.set)

    def feed_app_watchdog(self):
        self.last_app_feed_timestamp = time.time()

    # --- TASK INJECTORS FOR METADATA ---

    def inject_unit_update(self, name, unit_enum):
        async def _do():
            obj = self.engine.get_app_object(name)
            if obj and hasattr(obj, 'units'):
                obj.units = unit_enum
        if self.loop: self.loop.call_soon_threadsafe(lambda: asyncio.create_task(_do()))

    def inject_description_update(self, name, desc_str):
        async def _do():
            obj = self.engine.get_app_object(name)
            if obj:
                obj.description = desc_str
        if self.loop: self.loop.call_soon_threadsafe(lambda: asyncio.create_task(_do()))

    def inject_state_text_update(self, name, texts: list):
        async def _do():
            obj = self.engine.get_app_object(name)
            if not obj: return

            if isinstance(obj, (BinaryValueObject, WritableBinaryValue)):
                if len(texts) >= 2:
                    obj.inactiveText = texts[0]
                    obj.activeText = texts[1]

            elif isinstance(obj, (MultiStateValueObject, WritableMultiStateValue)):
                # Mise à jour MSV: stateText + numberOfStates
                obj.numberOfStates = len(texts)
                obj.stateText = [CharacterString(s) for s in texts]

        if self.loop: self.loop.call_soon_threadsafe(lambda: asyncio.create_task(_do()))


# --- SECTION 5: FACADE (API PUBLIQUE) ---

class BACnetInterface:
    """Façade principale à instancier par l'application."""

    def __init__(self,
                 app_logger: logging.Logger,
                 unit_mapping: Dict[str, int],
                 device_id: int,
                 ip_address: str,
                 port: int = DEFAULT_PORT,
                 vendor_id: int = DEFAULT_VENDOR_ID,
                 vendor_name: str = "PythonApp",
                 app_watchdog_timeout: int = 60):

        self.logger = app_logger
        self.unit_mapper = UnitMapper(unit_mapping)
        self.device_id = device_id
        self.ip_address = ip_address
        self.port = port
        self.vendor_id = vendor_id
        self.vendor_name = vendor_name
        self.app_watchdog_timeout = app_watchdog_timeout

        self._config_objects: List[dict] = []
        self._declared_names = set()
        self._service: Optional[BACnetService] = None
        self._update_lock = threading.Lock()
        self._pending_updates: Dict[str, Tuple] = {}

    def declare_object(self,
                       name: str,
                       type: str,
                       writable: bool,
                       description: str = "",
                       units: str = None,
                       state_texts: List[str] = None,
                       active_text: str = None,
                       inactive_text: str = None,
                       initial_value = None):

        if name in self._declared_names:
            raise ValueError(f"BACnet Object name '{name}' already declared.")
        if type not in ['AV', 'BV', 'MSV']:
            raise ValueError(f"Unknown type '{type}'. Use AV, BV, or MSV.")

        cfg = {
            'name': name,
            'type': type,
            'writable': writable,
            'description': description,
            'units': units,
            'state_texts': state_texts,
            'active_text': active_text,
            'inactive_text': inactive_text,
            'initial_value': initial_value
        }
        self._config_objects.append(cfg)
        self._declared_names.add(name)

    def start(self, timeout=5.0):
        if self._service and self._service.is_alive(): return
        self.logger.info(f"Starting BACnet Service (Spec V{SPEC_VERSION})...")

        self._service = BACnetService(
            interface=self,
            device_id=self.device_id,
            ip_address=self.ip_address,
            port=self.port,
            vendor_id=self.vendor_id,
            vendor_name=self.vendor_name,
            model_name="BacnetPythonServer",
            config_objects=self._config_objects,
            app_watchdog_timeout=self.app_watchdog_timeout
        )
        self._service.start()

        if not self._service.ready_event.wait(timeout=timeout):
            err = self._service.fatal_error
            self.stop()
            if err: raise RuntimeError(f"BACnet Start Failed: {err}") from err
            else: raise TimeoutError("BACnet Service timed out.")

    def stop(self):
        if self._service:
            self.logger.info("Stopping BACnet Service...")
            self._service.stop()
            self._service.join(timeout=2.0)
            self._service = None
            gc.collect()

    def restart(self):
        self.stop()
        time.sleep(0.5)
        self.start()

    def feed_watchdog(self):
        if self._service: self._service.feed_app_watchdog()

    def update(self, name: str, value=None, is_error=None, is_manual=None, is_alarm=None):
        if name not in self._declared_names: return
        with self._update_lock:
            self._pending_updates[name] = (value, is_error, is_manual, is_alarm)

    def pop_pending_updates(self) -> Dict[str, Tuple]:
        with self._update_lock:
            if not self._pending_updates: return {}
            data = self._pending_updates
            self._pending_updates = {}
            return data

    # --- DYNAMIC CONFIGURATION API (V1.2) ---

    def set_unit(self, name: str, unit_str: str):
        """Mise à jour dynamique de l'unité (AV)."""
        if self._service and self._service.is_alive():
            unit_enum = self.unit_mapper.get_bacnet_unit(unit_str)
            self._service.inject_unit_update(name, unit_enum)

    def set_description(self, name: str, description: str):
        """Mise à jour dynamique de la description."""
        if self._service and self._service.is_alive():
            self._service.inject_description_update(name, description)

    def set_state_texts(self, name: str, texts: List[str]):
        """Mise à jour dynamique des libellés (BV: [Off, On], MSV: [States...])."""
        if self._service and self._service.is_alive():
            if texts and isinstance(texts, list):
                self._service.inject_state_text_update(name, texts)
            else:
                self.logger.warning(f"Invalid state texts format for {name}: {texts}")

    # --- DIAGNOSTIC ---

    def is_healthy(self) -> bool:
        if not self._service or not self._service.is_alive(): return False
        if self._service.fatal_error: return False
        if (time.time() - self._service.last_alive_timestamp) > 2.0: return False
        return True

    def get_last_error(self) -> Optional[Exception]:
        if self._service: return self._service.fatal_error
        return None

    def get_object_details(self, name: str) -> dict:
        if not self._service or not self._service.engine: return {}
        obj = self._service.engine.get_app_object(name)
        if not obj: return {}

        details = {
            'presentValue': obj.presentValue,
            'reliability': str(obj.reliability),
            'statusFlags': obj.statusFlags,
            'description': obj.description,
            'outOfService': obj.outOfService
        }
        if hasattr(obj, 'units'): details['units'] = str(obj.units)
        if hasattr(obj, 'activeText'): details['activeText'] = obj.activeText
        if hasattr(obj, 'inactiveText'): details['inactiveText'] = obj.inactiveText
        if hasattr(obj, 'stateText'): details['stateText'] = [str(s) for s in obj.stateText]

        return details

    def get_value(self, name: str) -> Any:
        """Retourne la 'presentValue' actuelle de l'objet (Lecture simple)."""
        if not self._service or not self._service.engine:
            return None
        obj = self._service.engine.get_app_object(name)
        if obj:
            return obj.presentValue
        return None

    def get_priority_array(self, name: str) -> Optional[List]:
        """Retourne le tableau des priorités (si objet Writable)."""
        if not self._service or not self._service.engine: return None
        obj = self._service.engine.get_app_object(name)
        if obj and hasattr(obj, 'priorityArray'):
            return list(obj.priorityArray)
        return None

    # --- HOOKS ---

    def on_persistence_load(self) -> Optional[dict]: return None
    def on_persistence_save(self, data: dict): pass
    def on_external_write(self, name: str, value: Any, priority: int): pass
    def on_server_error(self, error: Exception): pass
