"""
QML Bridge Module.
==================

Acts as the mediator between Python business logic and the QML UI layer.
Exposes properties and signals that QML can bind to for real-time updates.
"""

from PySide6.QtCore import QObject, Property, Signal, Slot, QTimer, QThread
import logging
import psutil
import threading
import time
import os
import weakref
from shiboken6 import isValid
try:
    import torch
except ImportError:
    torch = None

from src.core.config import ConfigManager

class StatsWorker(QThread):
    stats_ready = Signal(float, float, float, float)

    def __init__(self):
        super().__init__()
        self.running = True
        self.process = psutil.Process()
        self.process.cpu_percent() # Prime
        
        # Initialize NVML for accurate GPU monitoring
        self.nvml_available = False
        self.gpu_handle = None
        try:
            import pynvml
            pynvml.nvmlInit()
            self.gpu_handle = pynvml.nvmlDeviceGetHandleByIndex(0)
            self.nvml_available = True
        except:
            pass

    def run(self):
        while self.running:
            try:
                # App CPU
                cpu = self.process.cpu_percent() / psutil.cpu_count()
                
                # App RAM in MB
                ram_bytes = self.process.memory_info().rss
                ram_mb = ram_bytes / (1024 * 1024)
                
                # GPU Stats via NVML (accurate for CTranslate2/faster-whisper)
                vram_percent = 0.0
                vram_mb = 0.0
                if self.nvml_available and self.gpu_handle:
                    try:
                        import pynvml
                        mem_info = pynvml.nvmlDeviceGetMemoryInfo(self.gpu_handle)
                        vram_mb = mem_info.used / (1024 * 1024)
                        vram_percent = (mem_info.used / mem_info.total * 100) if mem_info.total > 0 else 0.0
                    except: pass

                self.stats_ready.emit(
                    round(cpu, 1),
                    round(ram_mb, 1),
                    round(vram_mb, 1),
                    round(vram_percent, 1)
                )
            except Exception:
                pass
            
            # Sleep that checks running flag more frequently
            for _ in range(10):
                if not self.running: break
                time.sleep(0.1)

    def stop(self):
        self.running = False
        # Cleanup NVML
        if self.nvml_available:
            try:
                import pynvml
                pynvml.nvmlShutdown()
            except: pass
        self.quit()
        self.wait(2000) # Wait up to 2 seconds for clean exit

class UIBridge(QObject):
    """
    Main controller exposed to QML.
    """
    # Signals for QML to listen to
    statusTextChanged = Signal(str)
    amplitudeChanged = Signal(float)
    isRecordingChanged = Signal(bool)
    isProcessingChanged = Signal(bool)
    hotkeysEnabledChanged = Signal(bool)
    isDownloadingChanged = Signal(bool)
    uiScaleChanged = Signal(float)
    appCpuChanged = Signal(float)
    appRamMbChanged = Signal(float)
    appVramMbChanged = Signal(float)
    appVramPercentChanged = Signal(float)
    downloadProgressChanged = Signal(float)
    loaderStatusChanged = Signal(str)
    toggleRecordingRequested = Signal()
    downloadRequested = Signal(str) # model name
    logAppended = Signal(str) # Emits new log line
    settingChanged = Signal(str, 'QVariant')
    modelStatesChanged = Signal() # Notify UI to re-check isModelDownloaded

    def __init__(self, parent=None):
        super().__init__(parent)
        self._status_text = "Ready"
        self._amplitude = 0.0
        self._is_recording = False
        self._is_processing = False
        self._download_progress = 0.0
        self._loader_status = "Scanning models..."
        self._is_recording = False
        self._hotkeys_enabled = True
        self._is_downloading = False
        self._ui_scale = float(ConfigManager().get("ui_scale"))
        self._logs = [] # Store last 1000 lines
        self._app_cpu = 0.0
        self._app_ram_mb = 0.0
        self._app_vram_mb = 0.0
        self._app_vram_percent = 0.0
        self._is_destroyed = False
        
        # Start QThread Stats Worker
        self.stats_worker = StatsWorker()
        self.stats_worker.stats_ready.connect(self.update_stats_callback)
        self.stats_worker.start()
        
        # Cleanup on destruction
        self.destroyed.connect(self._handle_destruction)

    def _handle_destruction(self, obj=None):
        self._is_destroyed = True
        # Explicitly disconnect signal BEFORE stopping the worker
        if hasattr(self, 'stats_worker'):
            try:
                self.stats_worker.stats_ready.disconnect(self.update_stats_callback)
            except: pass
            self.stats_worker.stop()

    @Property(bool, notify=hotkeysEnabledChanged)
    def hotkeysEnabled(self):
        return self._hotkeys_enabled

    @hotkeysEnabled.setter
    def hotkeysEnabled(self, val):
        if self._hotkeys_enabled != val:
            self._hotkeys_enabled = val
            self.hotkeysEnabledChanged.emit(val)
        
    @Slot(str)
    def append_log(self, line):
        self._logs.append(line)
        if len(self._logs) > 1000:
            self._logs.pop(0)
        self.logAppended.emit(line)

    @Property(str, notify=logAppended) # Simple full text getter
    def allLogs(self):
        return "\n".join(self._logs)


    @Slot(result=None)
    def toggle_recording(self):
        """Called by UI elements to trigger the app's recording logic."""
        # Removed debug prints for cleaner logs
        self.toggleRecordingRequested.emit() 

    # --- Properties ---
    
    @Property(float, notify=downloadProgressChanged)
    def downloadProgress(self): return self._download_progress
    
    @downloadProgress.setter
    def downloadProgress(self, val):
        self._download_progress = val
        self.downloadProgressChanged.emit(val)

    @Property(str, notify=loaderStatusChanged)
    def loaderStatus(self): return self._loader_status

    @loaderStatus.setter
    def loaderStatus(self, val):
        self._loader_status = val
        self.loaderStatusChanged.emit(val)
    
    @Property(str, notify=statusTextChanged)
    def statusText(self):
        return self._status_text

    @statusText.setter
    def statusText(self, val):
        if self._status_text != val:
            self._status_text = val
            self.statusTextChanged.emit(val)

    @Property(float, notify=amplitudeChanged)
    def amplitude(self):
        return self._amplitude

    @amplitude.setter
    def amplitude(self, val):
        if self._amplitude != val:
            self._amplitude = val
            self.amplitudeChanged.emit(val)

    @Property(bool, notify=isRecordingChanged)
    def isRecording(self):
        return self._is_recording

    @isRecording.setter
    def isRecording(self, val):
        if self._is_recording != val:
            self._is_recording = val
            self.isRecordingChanged.emit(val)

    @Property(bool, notify=isProcessingChanged)
    def isProcessing(self):
        return self._is_processing

    @isProcessing.setter
    def isProcessing(self, val):
        if self._is_processing != val:
            self._is_processing = val
            self.isProcessingChanged.emit(val)

    # --- Methods called from Python logic ---
    
    @Slot(str)
    def update_status(self, text):
        self.statusText = text

    @Slot(float)
    def update_amplitude(self, amp):
        self.amplitude = amp

    # --- Methods called from QML ---
    
    @Slot(result=list)
    def get_supported_languages(self):
        from src.core.languages import get_language_names
        return get_language_names()

    @Slot(str)
    def set_language_by_name(self, name):
        from src.core.languages import get_code_by_name
        from src.core.config import ConfigManager
        code = get_code_by_name(name)
        ConfigManager().set("language", code)
        self.settingChanged.emit("language", code)
        
    @Slot(result=str)
    def get_current_language_name(self):
        from src.core.languages import get_name_by_code
        from src.core.config import ConfigManager
        code = ConfigManager().get("language")
        return get_name_by_code(code)

    @Slot(str, result='QVariant')
    def getSetting(self, key):
        from src.core.config import ConfigManager
        return ConfigManager().get(key)

    @Slot(str, 'QVariant')
    def setSetting(self, key, value):
        from src.core.config import ConfigManager
        ConfigManager().set(key, value)
        if key == "ui_scale":
            self.uiScale = float(value)
        self.settingChanged.emit(key, value) # Notify listeners (e.g. Overlay)
        
    @Property(float, notify=uiScaleChanged)
    def uiScale(self): return self._ui_scale

    @uiScale.setter
    def uiScale(self, val):
        if self._ui_scale != val:
            self._ui_scale = val
            self.uiScaleChanged.emit(val)

    @Property(float, notify=appCpuChanged)
    def appCpu(self): return self._app_cpu

    @Property(float, notify=appRamMbChanged)
    def appRamMb(self): return self._app_ram_mb

    @Property(float, notify=appVramMbChanged)
    def appVramMb(self): return self._app_vram_mb

    @Property(float, notify=appVramPercentChanged)
    def appVramPercent(self): return self._app_vram_percent

    @Slot(float, float, float, float)
    def update_stats_callback(self, cpu, ram_mb, vram_mb, vram_p):
        """Called from the background StatsWorker thread."""
        # Root-level try-except to catch any decorator/runtime errors during shutdown
        try:
            if self._is_destroyed or not isValid(self):
                return
                
            if self._app_cpu != cpu:
                self._app_cpu = cpu
                self.appCpuChanged.emit(cpu)

            if self._app_ram_mb != ram_mb:
                self._app_ram_mb = ram_mb
                self.appRamMbChanged.emit(ram_mb)

            if self._app_vram_mb != vram_mb:
                self._app_vram_mb = vram_mb
                self.appVramMbChanged.emit(vram_mb)

            if self._app_vram_percent != vram_p:
                self._app_vram_percent = vram_p
                self.appVramPercentChanged.emit(vram_p)
        except:
            pass
        
    @Slot(result='QVariantList')
    def getAudioDevices(self):
        """Returns a list of audio input devices. Cached to prevent UI blocking."""
        print("[Bridge] getAudioDevices called.")
        if hasattr(self, '_cached_devices'):
            print(f"[Bridge] Returning cached devices ({len(self._cached_devices)} found)")
            return self._cached_devices

        # If we are here, it's the first load. 
        # Since QML expects a return value immediately, we return a placeholder or empty list
        # and start a thread to populate it for next time (or emission).
        # However, for a settings menu, "loading..." is better than blocking.
        # But to properly fix the 5s block, we MUST NOT import sounddevice on the main thread if it's slow.
        
        print("[Bridge] No cached devices yet! Returning empty list.")
        return []

    def preload_audio_devices(self):
        """Called on startup in a worker thread."""
        try:
            import sounddevice as sd
            devices = []
            device_list = sd.query_devices()
            for i, dev in enumerate(device_list):
                 if dev['max_input_channels'] > 0:
                        devices.append({"id": i, "name": dev['name']})
            self._cached_devices = devices
            logging.info(f"Audio devices preloaded: {len(devices)} found")
        except Exception as e:
            logging.error(f"Failed to preload audio devices: {e}")

    @Slot()
    def toggle_recording(self):
        """Called by UI elements to trigger the app's recording logic."""
        # This will be connected to the main app's toggle logic
        pass 
    @Property(bool, notify=isDownloadingChanged)
    def isDownloading(self): return self._is_downloading

    @isDownloading.setter
    def isDownloading(self, val):
        if self._is_downloading != val:
            self._is_downloading = val
            self.isDownloadingChanged.emit(val)

    @Slot(str, result=bool)
    def isModelDownloaded(self, size):
        if not size: return False
        
        try:
            from src.core.paths import get_models_path
            # Check new simple format used by DownloadWorker
            path_simple = get_models_path() / f"faster-whisper-{size}"
            if path_simple.exists() and any(path_simple.iterdir()):
                return True
            
            # Check HF Cache format (legacy/default)
            folder_name = f"models--Systran--faster-whisper-{size}"
            path_hf = get_models_path() / folder_name
            snapshots = path_hf / "snapshots"
            if snapshots.exists() and any(snapshots.iterdir()):
                return True
                
            # Check direct folder (simple)
            path_direct = get_models_path() / size
            if (path_direct / "config.json").exists():
                return True
                
        except Exception as e:
            logging.error(f"Error checking model status: {e}")
            
        return False

    @Slot(str)
    def downloadModel(self, size):
        self.downloadRequested.emit(size)

    @Slot()
    def notifyModelStatesChanged(self):
        self.modelStatesChanged.emit()
