"""
CO₂ monitoring and analysis for cave systems.
Tracks carbon dioxide levels, ventilation, and respiratory hazards.

Author: Samir Baladi
"""

import numpy as np
from typing import Dict, List, Tuple, Optional, Union
from dataclasses import dataclass
import logging

logger = logging.getLogger(__name__)

@dataclass
class CO2Reading:
    """Represents a CO₂ reading."""
    timestamp: float
    concentration: float  # ppm
    location: str
    depth: float  # meters below surface
    sensor_id: str


class CO2Monitor:
    """
    Monitors and analyzes cave CO₂ levels.
    """
    
    # CO₂ thresholds from paper
    THRESHOLDS = {
        'normal': 1000,      # ppm - normal outdoor air
        'elevated': 2000,    # ppm - noticeable
        'high': 5000,        # ppm - respiratory effects
        'dangerous': 8500,   # ppm - hazardous
        'critical': 15000    # ppm - immediately dangerous
    }
    
    def __init__(self):
        """Initialize CO₂ monitor."""
        self.readings = []
        self.sensors = {}
        logger.info("Initialized CO2Monitor")

    def add_reading(self,
                   timestamp: float,
                   concentration: float,
                   location: str,
                   depth: float,
                   sensor_id: str) -> None:
        """
        Add a CO₂ reading.
        
        Args:
            timestamp: Reading time
            concentration: CO₂ concentration (ppm)
            location: Measurement location
            depth: Depth below surface (m)
            sensor_id: Sensor identifier
        """
        reading = CO2Reading(
            timestamp=timestamp,
            concentration=concentration,
            location=location,
            depth=depth,
            sensor_id=sensor_id
        )
        
        self.readings.append(reading)
        
        # Update sensor info
        if sensor_id not in self.sensors:
            self.sensors[sensor_id] = {
                'location': location,
                'depth': depth,
                'first_reading': timestamp,
                'n_readings': 0
            }
        self.sensors[sensor_id]['n_readings'] += 1
        
        logger.debug(f"Added CO₂ reading: {concentration}ppm at {location}")

    def calculate_statistics(self,
                            location: Optional[str] = None,
                            start_time: Optional[float] = None,
                            end_time: Optional[float] = None) -> Dict[str, float]:
        """
        Calculate CO₂ statistics.
        
        Args:
            location: Filter by location
            start_time: Start time
            end_time: End time
            
        Returns:
            Dictionary with statistics
        """
        readings = self._filter_readings(location, start_time, end_time)
        
        if not readings:
            return {}
        
        values = [r.concentration for r in readings]
        
        return {
            'mean': round(np.mean(values)),
            'std': round(np.std(values)),
            'min': round(np.min(values)),
            'max': round(np.max(values)),
            'median': round(np.median(values)),
            'range': round(np.max(values) - np.min(values)),
            'n_readings': len(readings)
        }

    def assess_hazard(self,
                     concentration: float,
                     time_exposed: float = 0) -> Dict[str, Union[str, float]]:
        """
        Assess respiratory hazard from CO₂ level.
        
        Args:
            concentration: CO₂ concentration (ppm)
            time_exposed: Time already exposed (minutes)
            
        Returns:
            Dictionary with hazard assessment
        """
        if concentration < self.THRESHOLDS['normal']:
            level = "NORMAL"
            color = "🟢"
            effects = "No effects"
            max_time = 480  # minutes (8 hours)
        elif concentration < self.THRESHOLDS['elevated']:
            level = "ELEVATED"
            color = "🟡"
            effects = "Noticeable, stuffy air"
            max_time = 240
        elif concentration < self.THRESHOLDS['high']:
            level = "HIGH"
            color = "🟠"
            effects = "Headache, drowsiness possible"
            max_time = 120
        elif concentration < self.THRESHOLDS['dangerous']:
            level = "DANGEROUS"
            color = "🔴"
            effects = "Headache, dizziness, increased breathing rate"
            max_time = 60
        elif concentration < self.THRESHOLDS['critical']:
            level = "VERY DANGEROUS"
            color = "🔴"
            effects = "Severe symptoms, impaired judgment"
            max_time = 15
        else:
            level = "IMMEDIATE DANGER"
            color = "⚫"
            effects = "Unconsciousness, death in minutes"
            max_time = 0
        
        # Calculate safe remaining time
        if time_exposed >= max_time:
            remaining = 0
            action = "IMMEDIATE EVACUATION"
        else:
            remaining = max_time - time_exposed
            if remaining < 15:
                action = "EVACUATE NOW"
            elif remaining < 30:
                action = "PREPARE TO EVACUATE"
            else:
                action = "MONITOR CLOSELY"
        
        return {
            'hazard_level': level,
            'color': color,
            'effects': effects,
            'max_safe_minutes': max_time,
            'time_exposed': time_exposed,
            'remaining_minutes': round(remaining, 1),
            'action': action,
            'concentration': concentration
        }

    def calculate_flux(self,
                      location: str,
                      time_window: float = 3600) -> Dict[str, float]:
        """
        Calculate CO₂ flux (rate of change).
        
        Args:
            location: Location to analyze
            time_window: Time window in seconds
            
        Returns:
            Dictionary with flux calculations
        """
        readings = [r for r in self.readings if r.location == location]
        
        if len(readings) < 2:
            return {}
        
        # Sort by time
        readings.sort(key=lambda x: x.timestamp)
        
        # Get recent readings
        latest_time = readings[-1].timestamp
        cutoff = latest_time - time_window
        recent = [r for r in readings if r.timestamp >= cutoff]
        
        if len(recent) < 2:
            recent = readings[-2:]
        
        times = np.array([r.timestamp for r in recent])
        values = np.array([r.concentration for r in recent])
        
        # Calculate rate
        dt = times[-1] - times[0]
        if dt > 0:
            rate = (values[-1] - values[0]) / dt * 3600  # ppm per hour
            rate_per_min = rate / 60  # ppm per minute
        else:
            rate = 0
            rate_per_min = 0
        
        # Instantaneous rate (gradient)
        if len(times) > 2:
            gradients = np.gradient(values, times)
            max_rate = np.max(np.abs(gradients)) * 3600
        else:
            max_rate = abs(rate)
        
        return {
            'mean_rate_ppm_per_hour': round(rate, 1),
            'mean_rate_ppm_per_min': round(rate_per_min, 2),
            'max_rate_ppm_per_hour': round(max_rate, 1),
            'time_window_hours': round(time_window / 3600, 1),
            'initial': round(values[0]),
            'final': round(values[-1]),
            'change': round(values[-1] - values[0])
        }

    def detect_co2_event(self,
                        location: str,
                        threshold: float = 2000,
                        min_duration: float = 300) -> List[Dict]:
        """
        Detect CO₂ events (excursions above threshold).
        
        Args:
            location: Location to analyze
            threshold: Threshold concentration (ppm)
            min_duration: Minimum duration in seconds
            
        Returns:
            List of detected events
        """
        readings = [r for r in self.readings if r.location == location]
        
        if len(readings) < 2:
            return []
        
        # Sort by time
        readings.sort(key=lambda x: x.timestamp)
        
        events = []
        in_event = False
        event_start = None
        event_values = []
        
        for reading in readings:
            if reading.concentration > threshold and not in_event:
                # Start of event
                in_event = True
                event_start = reading.timestamp
                event_values = [reading.concentration]
            elif reading.concentration > threshold and in_event:
                # Continue event
                event_values.append(reading.concentration)
            elif reading.concentration <= threshold and in_event:
                # End of event
                event_end = reading.timestamp
                duration = event_end - event_start
                
                if duration >= min_duration:
                    events.append({
                        'start_time': event_start,
                        'end_time': event_end,
                        'duration_min': round(duration / 60, 1),
                        'peak': max(event_values),
                        'mean': round(np.mean(event_values)),
                        'location': location
                    })
                
                in_event = False
                event_values = []
        
        return events

    def calculate_ventilation_rate(self,
                                  location: str,
                                  external_co2: float = 420) -> float:
        """
        Estimate ventilation rate from CO₂ decay.
        
        Args:
            location: Location to analyze
            external_co2: External CO₂ concentration (ppm)
            
        Returns:
            Ventilation rate (air changes per hour)
        """
        readings = [r for r in self.readings if r.location == location]
        
        if len(readings) < 10:
            return 0
        
        # Sort by time
        readings.sort(key=lambda x: x.timestamp)
        
        # Find a decreasing trend
        values = np.array([r.concentration for r in readings])
        
        # Look for exponential decay
        # C(t) = C_eq + (C0 - C_eq) * exp(-k*t)
        
        # Simplified - assume decay to external
        if values[-1] < values[0] * 0.5:
            # Significant decrease detected
            time_span = readings[-1].timestamp - readings[0].timestamp
            if time_span > 0:
                # Time constant
                decay_ratio = (values[-1] - external_co2) / (values[0] - external_co2)
                if decay_ratio > 0:
                    k = -np.log(decay_ratio) / time_span * 3600  # per hour
                    return round(k, 2)
        
        return 0

    def predict_co2_buildup(self,
                           current_co2: float,
                           generation_rate: float,
                           ventilation_rate: float,
                           time_hours: float) -> np.ndarray:
        """
        Predict CO₂ buildup over time.
        
        Args:
            current_co2: Current CO₂ (ppm)
            generation_rate: CO₂ generation rate (ppm/hour)
            ventilation_rate: Ventilation rate (air changes/hour)
            time_hours: Time to predict (hours)
            
        Returns:
            Array of predicted CO₂ concentrations
        """
        times = np.linspace(0, time_hours, 100)
        
        # Equilibrium concentration
        external_co2 = 420
        equilibrium = external_co2 + generation_rate / ventilation_rate if ventilation_rate > 0 else float('inf')
        
        # Exponential approach to equilibrium
        if ventilation_rate > 0:
            co2 = equilibrium + (current_co2 - equilibrium) * np.exp(-ventilation_rate * times)
        else:
            co2 = current_co2 + generation_rate * times
        
        return co2

    def _filter_readings(self,
                        location: Optional[str] = None,
                        start_time: Optional[float] = None,
                        end_time: Optional[float] = None) -> List[CO2Reading]:
        """Filter readings by criteria."""
        filtered = self.readings
        
        if location:
            filtered = [r for r in filtered if r.location == location]
        
        if start_time:
            filtered = [r for r in filtered if r.timestamp >= start_time]
        
        if end_time:
            filtered = [r for r in filtered if r.timestamp <= end_time]
        
        return filtered

    def summary(self) -> Dict[str, Union[int, float, str, Dict]]:
        """Get summary of CO₂ monitoring."""
        if not self.readings:
            return {'status': 'No data'}
        
        overall_stats = self.calculate_statistics()
        
        # Get latest readings by location
        latest_by_location = {}
        locations = set(r.location for r in self.readings)
        for loc in locations:
            loc_readings = [r for r in self.readings if r.location == loc]
            if loc_readings:
                latest = max(loc_readings, key=lambda x: x.timestamp)
                latest_by_location[loc] = {
                    'co2': latest.concentration,
                    'time': latest.timestamp,
                    'hazard': self.assess_hazard(latest.concentration)
                }
        
        # Recent events
        recent_events = []
        for loc in locations:
            events = self.detect_co2_event(loc, threshold=2000, min_duration=300)
            if events:
                recent_events.extend(events[-3:])  # Last 3 events per location
        
        return {
            'n_readings': len(self.readings),
            'n_locations': len(locations),
            'n_sensors': len(self.sensors),
            'overall_stats': overall_stats,
            'latest_by_location': latest_by_location,
            'recent_events': recent_events,
            'thresholds': self.THRESHOLDS
        }


# Example usage
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    print("="*60)
    print("CAVORA - CO₂ Monitoring")
    print("="*60)
    
    co2m = CO2Monitor()
    
    # Add readings
    import time
    current = time.time()
    for i in range(10):
        co2m.add_reading(current + i*300, 850 + np.random.randn()*50 + i*20,
                        "Main Passage", 50, "C001")
    
    # Assess hazard
    hazard = co2m.assess_hazard(concentration=1200, time_exposed=30)
    print(f"\nHazard Assessment:")
    print(f"  {hazard['color']} {hazard['hazard_level']}")
    print(f"  {hazard['effects']}")
    print(f"  Action: {hazard['action']}")
    
    # Calculate flux
    flux = co2m.calculate_flux("Main Passage")
    print(f"\nCO₂ Flux:")
    print(f"  Rate: {flux.get('mean_rate_ppm_per_hour', 0)} ppm/hour")
    print(f"  Change: {flux.get('change', 0)} ppm")
    
    # Detect events
    events = co2m.detect_co2_event("Main Passage", threshold=1000)
    print(f"\nCO₂ Events: {len(events)} detected")
    
    print("\n" + "="*60)
