"""
Cave Passage Data Logger (CPDL) Interface.
Handles data acquisition from field sensors and data loggers.

Author: Samir Baladi
"""

import numpy as np
from typing import Dict, List, Tuple, Optional, Union
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
import json
import csv

logger = logging.getLogger(__name__)

@dataclass
class SensorReading:
    """Represents a single sensor reading."""
    timestamp: float  # Unix timestamp or year fraction
    sensor_id: str
    sensor_type: str  # 'temperature', 'humidity', 'co2', 'water_level'
    value: float
    unit: str
    quality: float  # 0-1, data quality score


class CPDLInterface:
    """
    Interface for Cave Passage Data Logger systems.
    Handles multiple sensor types and data formats.
    """
    
    SENSOR_TYPES = {
        'temperature': {'unit': '°C', 'precision': 0.1, 'range': (-10, 30)},
        'humidity': {'unit': '%', 'precision': 2, 'range': (0, 100)},
        'co2': {'unit': 'ppm', 'precision': 50, 'range': (400, 10000)},
        'water_level': {'unit': 'm', 'precision': 0.01, 'range': (0, 10)},
        'discharge': {'unit': 'm³/s', 'precision': 0.01, 'range': (0, 50)}
    }
    
    def __init__(self, data_dir: Optional[str] = None):
        """
        Initialize CPDL interface.
        
        Args:
            data_dir: Directory for data storage
        """
        self.data_dir = data_dir
        self.sensors = {}
        self.readings = []
        self.active_loggers = set()
        logger.info(f"Initialized CPDLInterface with data_dir={data_dir}")

    def register_sensor(self,
                       sensor_id: str,
                       sensor_type: str,
                       location: str,
                       sampling_rate: float) -> None:
        """
        Register a new sensor.
        
        Args:
            sensor_id: Unique sensor identifier
            sensor_type: Type of sensor
            location: Installation location
            sampling_rate: Sampling rate (Hz)
        """
        if sensor_type not in self.SENSOR_TYPES:
            raise ValueError(f"Unknown sensor type: {sensor_type}")
        
        self.sensors[sensor_id] = {
            'type': sensor_type,
            'location': location,
            'sampling_rate': sampling_rate,
            'unit': self.SENSOR_TYPES[sensor_type]['unit'],
            'precision': self.SENSOR_TYPES[sensor_type]['precision'],
            'installed': datetime.now().timestamp(),
            'active': True
        }
        
        logger.info(f"Registered sensor {sensor_id} ({sensor_type}) at {location}")

    def add_reading(self,
                   sensor_id: str,
                   timestamp: float,
                   value: float,
                   quality: float = 1.0) -> None:
        """
        Add a sensor reading.
        
        Args:
            sensor_id: Sensor identifier
            timestamp: Reading time
            value: Measured value
            quality: Data quality score (0-1)
        """
        if sensor_id not in self.sensors:
            logger.warning(f"Unknown sensor: {sensor_id}")
            return
        
        sensor_info = self.sensors[sensor_id]
        
        # Validate value range
        vmin, vmax = self.SENSOR_TYPES[sensor_info['type']]['range']
        if value < vmin or value > vmax:
            logger.warning(f"Value {value} outside expected range [{vmin}, {vmax}]")
            quality *= 0.8
        
        reading = SensorReading(
            timestamp=timestamp,
            sensor_id=sensor_id,
            sensor_type=sensor_info['type'],
            value=value,
            unit=sensor_info['unit'],
            quality=quality
        )
        
        self.readings.append(reading)
        logger.debug(f"Added reading from {sensor_id}: {value} {sensor_info['unit']}")

    def get_readings(self,
                    sensor_id: Optional[str] = None,
                    sensor_type: Optional[str] = None,
                    start_time: Optional[float] = None,
                    end_time: Optional[float] = None) -> List[SensorReading]:
        """
        Get readings with optional filtering.
        
        Args:
            sensor_id: Filter by sensor ID
            sensor_type: Filter by sensor type
            start_time: Start time filter
            end_time: End time filter
            
        Returns:
            List of filtered readings
        """
        filtered = self.readings
        
        if sensor_id:
            filtered = [r for r in filtered if r.sensor_id == sensor_id]
        
        if sensor_type:
            filtered = [r for r in filtered if r.sensor_type == sensor_type]
        
        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 calculate_statistics(self,
                            sensor_id: str,
                            start_time: Optional[float] = None,
                            end_time: Optional[float] = None) -> Dict[str, float]:
        """
        Calculate statistics for a sensor.
        
        Args:
            sensor_id: Sensor identifier
            start_time: Start time
            end_time: End time
            
        Returns:
            Dictionary with statistics
        """
        readings = self.get_readings(sensor_id=sensor_id, 
                                    start_time=start_time, 
                                    end_time=end_time)
        
        if not readings:
            return {}
        
        values = [r.value for r in readings]
        times = [r.timestamp for r in readings]
        
        # Calculate rate of change
        if len(times) > 1:
            sorted_idx = np.argsort(times)
            sorted_times = np.array(times)[sorted_idx]
            sorted_values = np.array(values)[sorted_idx]
            
            rates = np.gradient(sorted_values, sorted_times)
            mean_rate = np.mean(rates)
            max_rate = np.max(np.abs(rates))
        else:
            mean_rate = 0
            max_rate = 0
        
        return {
            'mean': round(np.mean(values), 2),
            'std': round(np.std(values), 2),
            'min': round(np.min(values), 2),
            'max': round(np.max(values), 2),
            'median': round(np.median(values), 2),
            'q1': round(np.percentile(values, 25), 2),
            'q3': round(np.percentile(values, 75), 2),
            'n_readings': len(readings),
            'mean_rate': round(mean_rate, 4),
            'max_rate': round(max_rate, 4),
            'data_quality': round(np.mean([r.quality for r in readings]), 2)
        }

    def detect_anomalies(self,
                        sensor_id: str,
                        threshold_std: float = 3.0) -> List[Dict]:
        """
        Detect anomalies in sensor data.
        
        Args:
            sensor_id: Sensor identifier
            threshold_std: Standard deviation threshold
            
        Returns:
            List of anomalies detected
        """
        readings = self.get_readings(sensor_id=sensor_id)
        
        if not readings:
            return []
        
        values = [r.value for r in readings]
        mean = np.mean(values)
        std = np.std(values)
        
        anomalies = []
        for reading in readings:
            z_score = abs(reading.value - mean) / std if std > 0 else 0
            if z_score > threshold_std:
                anomalies.append({
                    'timestamp': reading.timestamp,
                    'value': reading.value,
                    'z_score': round(z_score, 2),
                    'expected': round(mean, 2),
                    'sensor_id': sensor_id
                })
        
        return anomalies

    def export_to_csv(self, filename: str, sensor_id: Optional[str] = None) -> None:
        """
        Export readings to CSV file.
        
        Args:
            filename: Output filename
            sensor_id: Optional sensor filter
        """
        readings = self.get_readings(sensor_id=sensor_id)
        
        with open(filename, 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerow(['timestamp', 'sensor_id', 'type', 'value', 'unit', 'quality'])
            
            for r in readings:
                writer.writerow([
                    r.timestamp,
                    r.sensor_id,
                    r.sensor_type,
                    r.value,
                    r.unit,
                    r.quality
                ])
        
        logger.info(f"Exported {len(readings)} readings to {filename}")

    def export_to_json(self, filename: str) -> None:
        """
        Export all data to JSON file.
        
        Args:
            filename: Output filename
        """
        data = {
            'sensors': self.sensors,
            'readings': [
                {
                    'timestamp': r.timestamp,
                    'sensor_id': r.sensor_id,
                    'sensor_type': r.sensor_type,
                    'value': r.value,
                    'unit': r.unit,
                    'quality': r.quality
                }
                for r in self.readings
            ]
        }
        
        with open(filename, 'w') as f:
            json.dump(data, f, indent=2)
        
        logger.info(f"Exported data to {filename}")

    def load_from_csv(self, filename: str) -> int:
        """
        Load readings from CSV file.
        
        Args:
            filename: Input filename
            
        Returns:
            Number of readings loaded
        """
        count = 0
        with open(filename, 'r') as f:
            reader = csv.DictReader(f)
            for row in reader:
                self.add_reading(
                    sensor_id=row['sensor_id'],
                    timestamp=float(row['timestamp']),
                    value=float(row['value']),
                    quality=float(row.get('quality', 1.0))
                )
                count += 1
        
        logger.info(f"Loaded {count} readings from {filename}")
        return count

    def get_sensor_status(self, sensor_id: str) -> Dict[str, Union[str, float]]:
        """
        Get current status of a sensor.
        
        Args:
            sensor_id: Sensor identifier
            
        Returns:
            Dictionary with sensor status
        """
        if sensor_id not in self.sensors:
            return {'error': 'Sensor not found'}
        
        sensor = self.sensors[sensor_id]
        readings = self.get_readings(sensor_id=sensor_id)
        
        if readings:
            last_reading = max(readings, key=lambda r: r.timestamp)
            time_since_last = datetime.now().timestamp() - last_reading.timestamp
            
            # Check if sensor is responding
            if time_since_last > 3600:  # 1 hour
                status = "WARNING - No recent data"
                color = "🟡"
            else:
                status = "ACTIVE"
                color = "🟢"
        else:
            last_reading = None
            time_since_last = float('inf')
            status = "NO DATA"
            color = "⚫"
        
        return {
            'sensor_id': sensor_id,
            'type': sensor['type'],
            'location': sensor['location'],
            'status': status,
            'color': color,
            'last_reading': last_reading.value if last_reading else None,
            'last_reading_time': last_reading.timestamp if last_reading else None,
            'time_since_last_hours': round(time_since_last / 3600, 1) if time_since_last != float('inf') else 'N/A',
            'unit': sensor['unit'],
            'sampling_rate': sensor['sampling_rate']
        }

    def summary(self) -> Dict[str, Union[int, float, str, List]]:
        """Get summary of monitoring data."""
        sensor_status = []
        for sensor_id in self.sensors:
            sensor_status.append(self.get_sensor_status(sensor_id))
        
        # Group readings by type
        readings_by_type = {}
        for reading in self.readings:
            if reading.sensor_type not in readings_by_type:
                readings_by_type[reading.sensor_type] = 0
            readings_by_type[reading.sensor_type] += 1
        
        return {
            'n_sensors': len(self.sensors),
            'n_readings': len(self.readings),
            'readings_by_type': readings_by_type,
            'sensor_status': sensor_status,
            'data_span_days': self._calculate_data_span()
        }

    def _calculate_data_span(self) -> float:
        """Calculate total time span of data."""
        if not self.readings:
            return 0
        
        times = [r.timestamp for r in self.readings]
        return (max(times) - min(times)) / 86400  # Convert to days


# Example usage
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    print("="*60)
    print("CAVORA - CPDL Interface")
    print("="*60)
    
    cpdl = CPDLInterface()
    
    # Register sensors
    cpdl.register_sensor("T001", "temperature", "Main Passage", 0.01)
    cpdl.register_sensor("H001", "humidity", "Main Passage", 0.01)
    cpdl.register_sensor("C001", "co2", "Main Passage", 0.001)
    
    # Add some readings
    import time
    current_time = time.time()
    for i in range(10):
        cpdl.add_reading("T001", current_time + i*3600, 11.5 + np.sin(i)*0.5)
        cpdl.add_reading("H001", current_time + i*3600, 95 + np.random.randn()*2)
        cpdl.add_reading("C001", current_time + i*3600, 850 + np.random.randn()*50)
    
    # Get statistics
    stats = cpdl.calculate_statistics("T001")
    print(f"\nTemperature Statistics:")
    for key, value in stats.items():
        print(f"  {key}: {value}")
    
    # Get sensor status
    status = cpdl.get_sensor_status("T001")
    print(f"\nSensor Status:")
    print(f"  {status['color']} {status['status']}")
    print(f"  Last reading: {status['last_reading']} {status['unit']}")
    
    print("\n" + "="*60)
