"""MonitoringPlatform for capturing motor feedback data.

This module provides a MonitoringPlatform that captures motor feedback data at full rate
(~100Hz per motor) and periodically dumps it to disk as Parquet files for later analysis.

The platform uses an event-based approach with double buffering to minimize performance
impact on the motor control loop.
"""

import os
import time
import threading
from collections import deque
from datetime import datetime

from feathersdk.utils.feathertypes import TYPE_CHECKING, NamedTuple

from .steppable_system import SteppableSystem
from .motors.robstride import RobstrideMotor, RobstrideMotorError

if TYPE_CHECKING:
    from .motors.motors_manager import MotorsManager


class MotorFeedbackRecord(NamedTuple):
    timestamp: float
    motor_name: str
    angle: float
    velocity: float
    torque: float
    temp: float
    mode: int
    errors: int


class MonitoringPlatform(SteppableSystem):
    """Platform for monitoring and logging motor feedback data.
    
    Captures all motor feedback events at full rate and periodically dumps
    them to disk as Parquet files. Uses double buffering to minimize lock
    contention during disk writes.
    
    Args:
        motors_manager: The MotorsManager instance to monitor
        config: Configuration dict with optional keys:
            - output_dir: Directory for log files (default: /feathersys/logs/feather_monitoring)
            - max_buffer_size: Maximum records in buffer (default: 500000)
    
    Example:
        config = {
            "monitoring": {
                "output_dir": "/feathersys/logs/feather_monitoring",
                "max_buffer_size": 500000,
            }
        }
    """

    def __init__(self, motors_manager: 'MotorsManager', config: dict = {}):
        super().__init__()
        # Once per minute dump frequency
        self.operating_frequency = 1 / 60
        self.motors_manager = motors_manager
        self._output_dir = config.get("output_dir", "/feathersys/logs/feather_monitoring")
        self._max_buffer_size = config.get("max_buffer_size", 500000)

        # Thread-safe buffer - deque with maxlen prevents unbounded growth
        self._buffer = deque(maxlen=self._max_buffer_size)
        self._buffer_lock = threading.Lock()

        self._enabled = False
        
        # Reference times for timestamp interpretation (set at session start)
        # These allow converting monotonic timestamps to wall-clock time during analysis
        self._reference_wall_time: float = 0.0
        self._reference_monotonic: float = 0.0
        
        # Session-specific output directory (set at session start)
        # Temporary fix to stop parquet files being made when testing
        self._session_dir = os.path.join(self._output_dir, "UNINITIALIZED")
    
    def _get_motor_feedback_record(self, motor: RobstrideMotor, timestamp: float) -> MotorFeedbackRecord:
        return MotorFeedbackRecord(
            timestamp=timestamp,
            motor_name=motor.motor_name,
            angle=motor.angle.value,
            velocity=motor.velocity.value,
            torque=motor.torque.value,
            temp=motor.temp.value,
            mode=motor.mode.value.value,
            errors=RobstrideMotorError.errors_to_int(motor.errors.value),
        )

    def _on_motor_feedback(self, motor: RobstrideMotor, timestamp: float) -> None:
        """Callback invoked by update_feedback - must be fast.
        
        Creates a tuple record and appends to the buffer with minimal overhead.
        Lock is O(1) and typically uncontended.
        
        Args:
            motor: The motor that received feedback
            timestamp: The timestamp of the feedback update
        """
        with self._buffer_lock:
            self._buffer.append(self._get_motor_feedback_record(motor, timestamp))

    def _step(self):
        """Called once per minute - drain buffer and write to disk."""
        self._dump_to_disk()

    def _dump_to_disk(self) -> None:
        """Dump buffered records to a Parquet file.
        
        Uses double buffering: swaps the buffer pointer (O(1) with lock held),
        then performs the expensive list() conversion and I/O outside the lock.
        This ensures update_feedback is never blocked during file writes.
        
        The Parquet file includes metadata with reference times for converting
        monotonic timestamps to wall-clock time during analysis.
        """
        # O(1) buffer swap - only holds lock for pointer assignment
        with self._buffer_lock:
            if not self._buffer:
                return
            old_buffer = self._buffer
            self._buffer = deque(maxlen=self._max_buffer_size)
        
        if not os.path.exists(self._session_dir):
            os.makedirs(self._session_dir, exist_ok=True)

        # list() and I/O happen OUTSIDE the lock - doesn't block update_feedback
        records = list(old_buffer)

        import pandas as pd
        import pyarrow as pa
        import pyarrow.parquet as pq

        columns = [
            "timestamp",
            "motor_name",
            "angle",
            "velocity",
            "torque",
            "temp",
            "mode",
            "errors",
        ]
        df = pd.DataFrame(records, columns=columns)
        
        # Convert to PyArrow table and add metadata for timestamp interpretation
        table = pa.Table.from_pandas(df)
        metadata = {
            b"reference_wall_time": str(self._reference_wall_time).encode(),
            b"reference_monotonic": str(self._reference_monotonic).encode(),
        }
        # Merge with existing metadata (if any)
        existing_metadata = table.schema.metadata or {}
        merged_metadata = {**existing_metadata, **metadata}
        table = table.replace_schema_metadata(merged_metadata)

        filename = f"motor_feedback_{datetime.now().strftime('%Y%m%d_%H%M%S')}.parquet"
        path = os.path.join(self._session_dir, filename)
        pq.write_table(table, path)

    def start(self, is_root_step: bool = False) -> None:
        """Start the monitoring platform and register the feedback listener.
        
        Captures reference times at session start to enable converting monotonic
        timestamps to wall-clock time during later analysis.
        
        Args:
            is_root_step: Whether this is the root stepping system
        """
        super().start(is_root_step)
        
        # Capture reference times for timestamp interpretation
        # These are captured close together to minimize drift between them
        self._reference_monotonic = time.monotonic()
        self._reference_wall_time = time.time()
        
        # Create session-specific subfolder based on reference wall time
        session_name = datetime.fromtimestamp(self._reference_wall_time).strftime("run_%Y-%m-%d_%H-%M-%S")
        self._session_dir = os.path.join(self._output_dir, session_name)
        os.makedirs(self._session_dir, exist_ok=True)
        
        # Register listener when platform starts
        RobstrideMotor.add_feedback_listener(self._on_motor_feedback)
        self._enabled = True

    def on_abort(self) -> None:
        """Handle abort by unregistering listener and flushing remaining data."""
        # Unregister listener and flush remaining data
        if self._enabled:
            RobstrideMotor.remove_feedback_listener(self._on_motor_feedback)
            self._enabled = False
        self._dump_to_disk()
