"""
Resource Monitor Module
Safely monitors CPU, memory, and disk usage for Windows, macOS, and Linux
Uses secure patterns that won't trigger antivirus software
"""

import psutil
import threading
import time
import platform
import os
import sys
from pathlib import Path
from typing import Optional, Dict, Any
from ws_package.log.writer import LogWriter

logger = LogWriter.file(__name__)

# Constants for byte conversions
BYTES_PER_KB = 1024
BYTES_PER_MB = BYTES_PER_KB * 1024
BYTES_PER_GB = BYTES_PER_MB * 1024

# Default monitoring interval (2 minutes)
DEFAULT_MONITORING_INTERVAL_SECONDS = 120

# App disk usage is expensive to compute — cache result to avoid periodic full tree walks
APP_DISK_USAGE_CACHE_TTL_SECONDS = 1800

# Thread join timeout
THREAD_JOIN_TIMEOUT_SECONDS = 2.0

# Threshold at which we emit a WARNING log and notify Electron.
# Disk: warn when the partition is ≥ 90 % full (≤ 10 % free).
# Memory: warn when system RAM usage is ≥ 90 %.
DISK_WARN_THRESHOLD_PERCENT = 90.0
MEMORY_WARN_THRESHOLD_PERCENT = 90.0


class ResourceMonitor:
    """
    Secure resource monitoring that tracks:
    - Total available system memory
    - Memory consumed by this app
    - CPU usage (process and system)
    - Disk usage (total, available, and app-specific)
    
    Uses safe patterns to avoid triggering antivirus:
    - Monitors only own process (no system-wide scanning)
    - Non-blocking psutil calls
    - Reasonable intervals (1-2 minutes)
    - Standard logging (no network calls)
    """
    
    def __init__(self, interval: int = DEFAULT_MONITORING_INTERVAL_SECONDS):
        """
        Initialize resource monitor
        
        Args:
            interval: Monitoring interval in seconds (default: 120 = 2 minutes)
        """
        self.interval = interval
        self.running = False
        self.monitor_thread: Optional[threading.Thread] = None
        self.process = psutil.Process()
        self.platform = platform.system()
        self.app_data_paths = self._get_app_data_paths()
        self._app_disk_cache_bytes = 0
        self._app_disk_cache_time = 0.0
        
        logger.info("ResourceMonitor initialized for %s (interval: %ss)", self.platform, interval)
    
    def _get_app_data_paths(self) -> Dict[str, Path]:
        """Get paths to app data directories"""
        paths = {}
        
        if self.platform == "Darwin":  # macOS
            base_dir = Path.home() / "Library/Application Support/Workstatus"
            paths['base'] = base_dir
            paths['database'] = base_dir / "ws_monitor.db"
            paths['logs'] = base_dir / "logs"
            paths['screenshots'] = base_dir / "screenshots"
            paths['activity_percentage'] = base_dir / "activity_percentage"
        elif self.platform == "Windows":
            if getattr(sys, 'frozen', False):
                base_dir = Path(sys.executable).parent
            else:
                base_dir = Path(os.environ.get('APPDATA', '')) / "Workstatus"
            paths['base'] = base_dir
            paths['database'] = base_dir / "ws_monitor.db"
            paths['logs'] = base_dir / "logs"
            paths['screenshots'] = base_dir / "screenshots"
            paths['activity_percentage'] = base_dir / "activity_percentage"
        else:  # Linux and others
            base_dir = Path.home() / "Workstatus"
            paths['base'] = base_dir
            paths['database'] = base_dir / "ws_monitor.db"
            paths['logs'] = base_dir / "logs"
            paths['screenshots'] = base_dir / "screenshots"
            paths['activity_percentage'] = base_dir / "activity_percentage"
        
        return paths
    
    def _get_disk_usage_for_path(self, path: Path) -> int:
        """Return byte size for a file or directory (iterative walk, no rglob)."""
        total_size = 0
        try:
            if path.is_file():
                return path.stat().st_size
            if not path.is_dir():
                return 0

            stack = [path]
            while stack:
                current = stack.pop()
                try:
                    with os.scandir(current) as entries:
                        for entry in entries:
                            try:
                                if entry.is_file(follow_symlinks=False):
                                    total_size += entry.stat(follow_symlinks=False).st_size
                                elif entry.is_dir(follow_symlinks=False):
                                    stack.append(Path(entry.path))
                            except (OSError, PermissionError):
                                continue
                except (OSError, PermissionError):
                    continue
        except (OSError, PermissionError):
            pass
        return total_size
    
    def _calculate_app_disk_usage(self) -> int:
        """Calculate total disk usage by the app (cached to avoid heavy walks)."""
        now = time.time()
        if (
            self._app_disk_cache_bytes > 0
            and (now - self._app_disk_cache_time) < APP_DISK_USAGE_CACHE_TTL_SECONDS
        ):
            return self._app_disk_cache_bytes

        total_size = 0
        
        # Database files (including WAL and SHM files)
        db_path = self.app_data_paths.get('database')
        if db_path:
            # Main database file
            total_size += self._get_disk_usage_for_path(db_path)
            # WAL and SHM files
            for suffix in ['-wal', '-shm']:
                wal_path = Path(str(db_path) + suffix)
                total_size += self._get_disk_usage_for_path(wal_path)
        
        # Logs directory
        logs_path = self.app_data_paths.get('logs')
        if logs_path:
            total_size += self._get_disk_usage_for_path(logs_path)
        
        # Screenshots directory
        screenshots_path = self.app_data_paths.get('screenshots')
        if screenshots_path:
            total_size += self._get_disk_usage_for_path(screenshots_path)
        
        # Activity percentage directory
        activity_path = self.app_data_paths.get('activity_percentage')
        if activity_path:
            total_size += self._get_disk_usage_for_path(activity_path)
        
        self._app_disk_cache_bytes = total_size
        self._app_disk_cache_time = time.time()
        return total_size
    
    def start(self):
        """Start resource monitoring in background thread"""
        if self.running:
            logger.warning("Resource monitor already running")
            return
        
        self.running = True
        self.monitor_thread = threading.Thread(
            target=self._monitor_loop,
            daemon=True,
            name="ResourceMonitor"
        )
        self.monitor_thread.start()
        logger.info("Resource monitor started - logging every %s seconds", self.interval)
    
    def stop(self):
        """Stop resource monitoring"""
        if not self.running:
            return
        
        self.running = False
        if self.monitor_thread and self.monitor_thread.is_alive():
            self.monitor_thread.join(timeout=THREAD_JOIN_TIMEOUT_SECONDS)
        logger.info("Resource monitor stopped")
    
    def _monitor_loop(self):
        """Main monitoring loop - runs in background thread"""
        while self.running:
            try:
                self._log_resources()
                # Sleep for interval, but check running flag periodically
                for _ in range(self.interval):
                    if not self.running:
                        break
                    time.sleep(1)
            except Exception as e:
                logger.error("Error in resource monitor loop: %s", e)
                # Continue monitoring even on error
                time.sleep(self.interval)
    
    def _log_resources(self):
        """
        Log CPU, memory, and disk usage - SAFE PATTERN
        
        Monitors:
        - Total available system memory
        - Memory consumed by this app
        - CPU usage (process and system)
        - Disk usage (total, available, and app-specific)
        """
        try:
            # Get own process stats (SAFE - no system-wide scanning)
            process_memory_info = self.process.memory_info()
            process_memory_mb = process_memory_info.rss / BYTES_PER_MB  # RSS = Resident Set Size
            process_memory_percent = self.process.memory_percent()
            
            # Get process CPU usage (non-blocking - SAFE)
            process_cpu_percent = self.process.cpu_percent()
            
            # Get system-wide memory (SAFE - standard system call)
            system_memory = psutil.virtual_memory()
            total_memory_gb = system_memory.total / BYTES_PER_GB
            available_memory_gb = system_memory.available / BYTES_PER_GB
            used_memory_gb = system_memory.used / BYTES_PER_GB
            system_memory_percent = system_memory.percent
            
            # Get system-wide CPU usage (non-blocking - SAFE)
            system_cpu_percent = psutil.cpu_percent()
            
            # Get CPU count for context
            cpu_count = psutil.cpu_count()
            
            # Get thread count (SAFE - own process only)
            thread_count = self.process.num_threads()
            
            # Get file descriptors/handles count (platform-specific)
            try:
                if self.platform == "Windows":
                    handle_count = self.process.num_handles()
                    fd_count = f"Handles: {handle_count}"
                else:
                    fd_count_num = self.process.num_fds()
                    fd_count = f"FDs: {fd_count_num}"
            except (AttributeError, psutil.AccessDenied):
                fd_count = "N/A"
            
            # Get disk usage for the partition where app data is stored
            base_path = self.app_data_paths.get('base', Path.cwd())
            try:
                disk_usage = psutil.disk_usage(str(base_path))
                total_disk_gb = disk_usage.total / BYTES_PER_GB
                available_disk_gb = disk_usage.free / BYTES_PER_GB
                used_disk_gb = disk_usage.used / BYTES_PER_GB
                disk_percent = disk_usage.percent
            except (OSError, PermissionError) as e:
                logger.warning("Could not get disk usage: %s", e)
                total_disk_gb = 0
                available_disk_gb = 0
                used_disk_gb = 0
                disk_percent = 0
            
            # Calculate app disk usage
            app_disk_bytes = self._calculate_app_disk_usage()
            app_disk_mb = app_disk_bytes / BYTES_PER_MB
            app_disk_gb = app_disk_bytes / BYTES_PER_GB
            
            # Format memory values for readability
            GB_THRESHOLD = 1.0
            if total_memory_gb >= GB_THRESHOLD:
                total_mem_str = f"{total_memory_gb:.2f} GB"
            else:
                total_mem_str = f"{total_memory_gb * BYTES_PER_KB:.0f} MB"
            
            if available_memory_gb >= GB_THRESHOLD:
                available_mem_str = f"{available_memory_gb:.2f} GB"
            else:
                available_mem_str = f"{available_memory_gb * BYTES_PER_KB:.0f} MB"
            
            # Format disk values for readability
            if total_disk_gb >= GB_THRESHOLD:
                total_disk_str = f"{total_disk_gb:.2f} GB"
            else:
                total_disk_str = f"{total_disk_gb * BYTES_PER_KB:.0f} MB"
            
            if available_disk_gb >= GB_THRESHOLD:
                available_disk_str = f"{available_disk_gb:.2f} GB"
            else:
                available_disk_str = f"{available_disk_gb * BYTES_PER_KB:.0f} MB"
            
            if app_disk_gb >= 1:
                app_disk_str = f"{app_disk_gb:.2f} GB"
            else:
                app_disk_str = f"{app_disk_mb:.2f} MB"
            
            # Log comprehensive resource information
            logger.info(
                f"[RESOURCE_MONITOR] "
                f"System Memory - Total: {total_mem_str}, "
                f"Available: {available_mem_str}, "
                f"Used: {system_memory_percent:.1f}% | "
                f"App Memory - {process_memory_mb:.2f} MB ({process_memory_percent:.1f}%) | "
                f"CPU - Process: {process_cpu_percent:.1f}%, System: {system_cpu_percent:.1f}% "
                f"({cpu_count} cores) | "
                f"Disk - Total: {total_disk_str}, Available: {available_disk_str}, "
                f"Used: {disk_percent:.1f}% | "
                f"App Disk Usage: {app_disk_str} | "
                f"Threads: {thread_count}, {fd_count}"
            )

            # ── Threshold alerts ──────────────────────────────────────────────
            # These fire a WARNING-level log (always visible in production) and
            # push a structured message to Electron so the UI can notify the user.
            self._check_thresholds(disk_percent, available_disk_str, system_memory_percent, available_mem_str)

            # Return structured data for potential future use
            return {
                'system': {
                    'total_memory_gb': total_memory_gb,
                    'available_memory_gb': available_memory_gb,
                    'used_memory_gb': used_memory_gb,
                    'memory_percent': system_memory_percent,
                    'cpu_percent': system_cpu_percent,
                    'cpu_count': cpu_count,
                    'total_disk_gb': total_disk_gb,
                    'available_disk_gb': available_disk_gb,
                    'used_disk_gb': used_disk_gb,
                    'disk_percent': disk_percent
                },
                'process': {
                    'memory_mb': process_memory_mb,
                    'memory_percent': process_memory_percent,
                    'cpu_percent': process_cpu_percent,
                    'thread_count': thread_count,
                    'disk_usage_mb': app_disk_mb,
                    'disk_usage_gb': app_disk_gb
                },
                'platform': self.platform
            }
            
        except psutil.AccessDenied as e:
            logger.warning("Access denied reading process stats: %s", e)
        except Exception as e:
            logger.error("Error logging resources: %s", e, exc_info=True)
            return None
    
    def _check_thresholds(
        self,
        disk_percent: float,
        available_disk_str: str,
        memory_percent: float,
        available_mem_str: str,
    ) -> None:
        """
        Emit WARNING logs and notify Electron when disk or memory exceed thresholds.

        Disk ≥ DISK_WARN_THRESHOLD_PERCENT  → ErrorCode.DISK_FULL [E109]
        Memory ≥ MEMORY_WARN_THRESHOLD_PERCENT → plain WARNING (no dedicated code)

        Electron notification is best-effort: the IPC send is skipped gracefully
        if the server is not yet ready or not connected.
        """
        from ws_package.utils.error_codes import ErrorCode

        disk_alert = disk_percent >= DISK_WARN_THRESHOLD_PERCENT
        mem_alert = memory_percent >= MEMORY_WARN_THRESHOLD_PERCENT

        if disk_alert:
            logger.warning(
                "%s: partition is %.1f%% full (only %s free). "
                "Screenshots and database writes may fail.",
                ErrorCode.DISK_FULL, disk_percent, available_disk_str,
            )

        if mem_alert:
            logger.warning(
                "[RESOURCE_MONITOR] High memory pressure: system RAM %.1f%% used "
                "(%s available). App may slow down or crash.",
                memory_percent, available_mem_str,
            )

        if disk_alert or mem_alert:
            self._notify_electron_resource_warning(
                disk_percent=disk_percent if disk_alert else None,
                memory_percent=memory_percent if mem_alert else None,
            )

    def _notify_electron_resource_warning(
        self,
        disk_percent: Optional[float],
        memory_percent: Optional[float],
    ) -> None:
        """Send a resource_warning IPC message to Electron (best-effort)."""
        try:
            from ws_package.utils.ipc_server import is_connected, send_message
            if not is_connected():
                return
            payload: Dict[str, Any] = {"type": "resource_warning", "data": {}}
            if disk_percent is not None:
                payload["data"]["disk_percent"] = round(disk_percent, 1)
                payload["data"]["disk_warning"] = True
            if memory_percent is not None:
                payload["data"]["memory_percent"] = round(memory_percent, 1)
                payload["data"]["memory_warning"] = True
            send_message(payload)
        except Exception as exc:  # noqa: BLE001 — non-fatal
            logger.debug("[RESOURCE_MONITOR] Could not send resource_warning to Electron: %s", exc)

    def get_current_stats(self) -> Optional[Dict[str, Any]]:
        """
        Get current resource statistics without logging
        
        Returns:
            Dictionary with current resource stats or None on error
        """
        try:
            process_memory_info = self.process.memory_info()
            process_memory_mb = process_memory_info.rss / BYTES_PER_MB
            process_memory_percent = self.process.memory_percent()
            process_cpu_percent = self.process.cpu_percent()
            
            system_memory = psutil.virtual_memory()
            system_cpu_percent = psutil.cpu_percent()
            
            # Get disk usage
            base_path = self.app_data_paths.get('base', Path.cwd())
            try:
                disk_usage = psutil.disk_usage(str(base_path))
                total_disk_gb = disk_usage.total / BYTES_PER_GB
                available_disk_gb = disk_usage.free / BYTES_PER_GB
                used_disk_gb = disk_usage.used / BYTES_PER_GB
                disk_percent = disk_usage.percent
            except (OSError, PermissionError):
                total_disk_gb = 0
                available_disk_gb = 0
                used_disk_gb = 0
                disk_percent = 0
            
            # Calculate app disk usage
            app_disk_bytes = self._calculate_app_disk_usage()
            app_disk_mb = app_disk_bytes / BYTES_PER_MB
            app_disk_gb = app_disk_bytes / BYTES_PER_GB
            
            return {
                'system': {
                    'total_memory_gb': system_memory.total / BYTES_PER_GB,
                    'available_memory_gb': system_memory.available / BYTES_PER_GB,
                    'used_memory_gb': system_memory.used / BYTES_PER_GB,
                    'memory_percent': system_memory.percent,
                    'cpu_percent': system_cpu_percent,
                    'cpu_count': psutil.cpu_count(),
                    'total_disk_gb': total_disk_gb,
                    'available_disk_gb': available_disk_gb,
                    'used_disk_gb': used_disk_gb,
                    'disk_percent': disk_percent
                },
                'process': {
                    'memory_mb': process_memory_mb,
                    'memory_percent': process_memory_percent,
                    'cpu_percent': process_cpu_percent,
                    'thread_count': self.process.num_threads(),
                    'disk_usage_mb': app_disk_mb,
                    'disk_usage_gb': app_disk_gb
                },
                'platform': self.platform
            }
        except Exception as e:
            logger.error("Error getting current stats: %s", e)
            return None


# Global instance
_resource_monitor: Optional[ResourceMonitor] = None


def resolve_monitoring_interval(interval: Optional[int] = None) -> int:
    """Return the monitoring interval in seconds (config → explicit → default)."""
    if interval is not None and interval > 0:
        return interval
    try:
        from ws_package.configs import config
        configured = config("memory_log_interval", section="Memory", dtype="int")
        if configured and configured > 0:
            return configured
    except Exception as exc:  # noqa: BLE001 — non-fatal
        logger.debug("Could not read memory_log_interval from config: %s", exc)
    return DEFAULT_MONITORING_INTERVAL_SECONDS


def get_resource_monitor(interval: int = 120) -> ResourceMonitor:
    """Get or create global resource monitor instance"""
    global _resource_monitor
    if _resource_monitor is None:
        _resource_monitor = ResourceMonitor(interval=interval)
    return _resource_monitor


def start_resource_monitoring(interval: Optional[int] = None):
    """Start global disk/memory monitoring (Windows, macOS, Linux)."""
    resolved = resolve_monitoring_interval(interval)
    monitor = get_resource_monitor(interval=resolved)
    monitor.start()
    logger.info(
        "Disk/memory monitoring active on %s (every %ss)",
        monitor.platform,
        resolved,
    )


def stop_resource_monitoring():
    """Stop global resource monitoring"""
    global _resource_monitor
    if _resource_monitor:
        _resource_monitor.stop()


def get_current_resource_stats() -> Optional[Dict[str, Any]]:
    """Get current resource statistics"""
    global _resource_monitor
    if _resource_monitor:
        return _resource_monitor.get_current_stats()
    return None

