"""
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:
- Ajout de set_description() pour mise à jour dynamique.
- Ajout de set_state_texts() pour mise à jour dynamique des libellés (BV/MSV).
"""

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
    from bacpypes3.local.device import LocalDeviceObject
    from bacpypes3.primitivedata import ObjectIdentifier, CharacterString, Unsigned
    from bacpypes3.basetypes import (
        EngineeringUnits, PropertyIdentifier, Reliability, StatusFlags,
        EventState, DeviceStatus, ServicesSupported, ObjectTypesSupported
    )
    from bacpypes3.object import (
        AnalogValueObject, BinaryValueObject, MultiStateValueObject,
        ReadableProperty, WritableProperty
    )
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: LocalDeviceObject, interface_ref):
        super().__init__(device_info, device_info.objectIdentifier[1])
        self.interface_ref = weakref.ref(interface_ref)
        self.log = interface_ref.logger
        self._objects_map = {}

    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):
        self.localDevice.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):
        try:
            self.logger.info(f"[BACnet] Initializing Device {self.device_id}...")
            local_device = LocalDeviceObject(
                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}"
            self.engine = BACnetServerEngine(local_device, 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("[BACnet] Server Ready.")

            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())
            ]
            await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)

        except Exception as e:
            self.fatal_error = e
            self.ready_event.set()
            raise e
        finally:
            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()
            await asyncio.sleep(0.01)
            time.sleep(0)

    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 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():
            elapsed = time.time() - self.last_app_feed_timestamp
            if elapsed > self.app_watchdog_timeout:
                if self.engine.localDevice.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.localDevice.systemStatus == DeviceStatus.nonOperational:
                    self.logger.info("[BACnet] App alive. System Operational.")
                    self.engine.set_system_status(DeviceStatus.operational)
            await asyncio.sleep(1)

    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

