"""
neuroflow.device -- Device abstraction for the NeuroFlow SDK.

Provides a high-level Device class that wraps both remote (API server)
and local (direct BrainFlow) device access behind a uniform interface.
Users interact with devices through start/stop/get_data methods without
needing to know the underlying transport.
"""

from __future__ import annotations

import time
from typing import Any, Optional

from neuroflow.models import Device as DeviceModel, DeviceStatus, Features, StreamData


class RemoteDevice:
    """
    A device managed by a remote NeuroFlow API server.

    This class is returned by ``Client.discover()`` and
    ``Client.connect()``. All operations are forwarded to the server
    via HTTP requests.

    Args:
        client: The parent Client instance for making API calls.
        info: Device model with current device information.
    """

    def __init__(self, client: Any, info: DeviceModel) -> None:
        self._client = client
        self._info = info

    @property
    def id(self) -> str:
        """Unique device identifier."""
        return self._info.device_id

    @property
    def name(self) -> str:
        """Human-readable device name."""
        return self._info.name

    @property
    def status(self) -> DeviceStatus:
        """Current device connection status."""
        return self._info.status

    @property
    def sampling_rate(self) -> int:
        """Sampling rate in Hz."""
        return self._info.sampling_rate

    @property
    def num_channels(self) -> int:
        """Number of EEG channels."""
        return self._info.num_channels

    @property
    def is_streaming(self) -> bool:
        """Whether the device is currently streaming."""
        return self._info.is_streaming

    @property
    def info(self) -> DeviceModel:
        """Full device information model."""
        return self._info

    def refresh(self) -> "RemoteDevice":
        """
        Refresh device info from the server.

        Returns:
            Self with updated device information.
        """
        self._info = self._client._get_device_info(self.id)
        return self

    def start_stream(self) -> dict[str, str]:
        """
        Start data streaming on this device.

        Returns:
            Server response dict with status and device_id.
        """
        result = self._client._start_device_stream(self.id)
        self._info.is_streaming = True
        self._info.status = DeviceStatus.STREAMING
        return result

    def stop_stream(self) -> dict[str, str]:
        """
        Stop data streaming on this device.

        Returns:
            Server response dict with status and device_id.
        """
        result = self._client._stop_device_stream(self.id)
        self._info.is_streaming = False
        self._info.status = DeviceStatus.CONNECTED
        return result

    def disconnect(self) -> dict[str, str]:
        """
        Disconnect the device from the server.

        Returns:
            Server response dict with status and device_id.
        """
        result = self._client._disconnect_device(self.id)
        self._info.status = DeviceStatus.DISCONNECTED
        self._info.is_streaming = False
        return result

    def get_impedance(self) -> dict:
        """
        Get electrode impedance measurements.

        Requires the device to be streaming. Uses hardware impedance
        if available, falls back to heuristic estimation.

        Returns:
            Dict with 'device_id', 'channels' (name -> kOhm),
            'timestamp', and 'quality_summary'.
        """
        return self._client.get_impedance(self.id)

    def __repr__(self) -> str:
        return (
            f"RemoteDevice(id={self.id!r}, name={self.name!r}, "
            f"status={self.status.value})"
        )


class LocalDevice:
    """
    A device managed directly via BrainFlow (no server required).

    This class wraps a NeuroFlow DeviceManager instance and exposes
    a simple interface for streaming and feature extraction.

    Args:
        device_manager: The underlying NeuroFlow DeviceManager instance.
        device_id: Identifier assigned to this device.
        board_id: BrainFlow board identifier.
    """

    def __init__(
        self,
        device_manager: Any,
        device_id: str,
        board_id: int,
    ) -> None:
        self._dm = device_manager
        self._device_id = device_id
        self._board_id = board_id

    @property
    def id(self) -> str:
        """Unique device identifier."""
        return self._device_id

    @property
    def name(self) -> str:
        """Human-readable device name."""
        if hasattr(self._dm, '_config') and self._dm._config:
            return self._dm._config.display_name
        return f"Board {self._board_id}"

    @property
    def board_id(self) -> int:
        """BrainFlow board identifier."""
        return self._board_id

    @property
    def sampling_rate(self) -> int:
        """Sampling rate in Hz."""
        return self._dm.sampling_rate

    @property
    def is_streaming(self) -> bool:
        """Whether the device is currently streaming."""
        return self._dm.is_streaming

    @property
    def is_connected(self) -> bool:
        """Whether the device is currently connected."""
        return self._dm.is_connected

    @property
    def status(self) -> str:
        """Current device state as a string."""
        return self._dm.state.value

    @property
    def total_samples(self) -> int:
        """Total samples received since streaming started."""
        return self._dm.total_samples

    def start_stream(self, poll_interval: float = 0.1) -> None:
        """
        Start data streaming from the device.

        Args:
            poll_interval: How often (seconds) to poll the BrainFlow buffer.
        """
        self._dm.start_stream(poll_interval=poll_interval)

    def stop_stream(self) -> None:
        """Stop data streaming (keeps the connection alive)."""
        self._dm.stop_stream()

    def get_data(self, num_samples: Optional[int] = None) -> StreamData:
        """
        Get data from the device buffer.

        Args:
            num_samples: Number of recent samples to retrieve.
                None returns all available data in the buffer.

        Returns:
            StreamData with EEG data, channel names, and quality info.
        """
        nfdf = self._dm.get_data(num_samples=num_samples)
        eeg = nfdf.eeg
        return StreamData(
            timestamp=time.time(),
            eeg=eeg.tolist() if hasattr(eeg, 'tolist') and eeg.size > 0 else [],
            channel_names=nfdf.channel_names,
            num_samples=nfdf.num_samples,
            sampling_rate=nfdf.sampling_rate,
            quality=nfdf.signal_quality,
            overall_quality=nfdf.overall_quality,
        )

    def get_current_data(self, seconds: float = 1.0) -> StreamData:
        """
        Get the most recent N seconds of data.

        Args:
            seconds: Number of seconds of recent data to retrieve.

        Returns:
            StreamData with the latest EEG samples.
        """
        nfdf = self._dm.get_current_data(seconds=seconds)
        eeg = nfdf.eeg
        return StreamData(
            timestamp=time.time(),
            eeg=eeg.tolist() if hasattr(eeg, 'tolist') and eeg.size > 0 else [],
            channel_names=nfdf.channel_names,
            num_samples=nfdf.num_samples,
            sampling_rate=nfdf.sampling_rate,
            quality=nfdf.signal_quality,
            overall_quality=nfdf.overall_quality,
        )

    def get_features(self, seconds: float = 2.0) -> Features:
        """
        Extract neuroscience features from recent data.

        Uses NeuroFlow's feature extraction pipeline to compute
        band powers, spectral features, and temporal statistics
        from the most recent data window.

        Args:
            seconds: Window size in seconds for feature extraction.

        Returns:
            Features with categorized neuroscience metrics.
        """
        nfdf = self._dm.get_current_data(seconds=seconds)
        eeg = nfdf.eeg

        if eeg.size == 0:
            return Features(session_id="", raw={})

        raw_features: dict[str, Any] = {}

        try:
            from neuroflow.processing.features import (
                feature_band_powers,
                feature_spectral,
                feature_temporal,
            )

            bp = feature_band_powers(eeg, nfdf.sampling_rate)
            raw_features.update(bp)

            sp = feature_spectral(eeg, nfdf.sampling_rate)
            raw_features.update(sp)

            tp = feature_temporal(eeg, nfdf.sampling_rate)
            raw_features.update(tp)

        except ImportError:
            # NeuroFlow processing not available -- return raw quality only
            raw_features = {
                f"quality_{name}": score
                for name, score in nfdf.signal_quality.items()
            }
            raw_features["overall_quality"] = nfdf.overall_quality

        return Features.from_raw_features(raw_features)

    def start_recording(self, session_id: str = "", subject_id: str = "") -> None:
        """
        Start recording data to an internal buffer.

        Args:
            session_id: Optional identifier for the recording session.
            subject_id: Optional identifier for the test subject.
        """
        self._dm.start_recording(session_id=session_id, subject_id=subject_id)

    def stop_recording(self) -> StreamData:
        """
        Stop recording and return all recorded data.

        Returns:
            StreamData containing the complete recording.
        """
        nfdf = self._dm.stop_recording()
        eeg = nfdf.eeg
        return StreamData(
            timestamp=time.time(),
            eeg=eeg.tolist() if hasattr(eeg, 'tolist') and eeg.size > 0 else [],
            channel_names=nfdf.channel_names,
            num_samples=nfdf.num_samples,
            sampling_rate=nfdf.sampling_rate,
            quality=nfdf.signal_quality,
            overall_quality=nfdf.overall_quality,
        )

    def get_impedance(self) -> dict[str, float]:
        """
        Get electrode impedance measurements per channel.

        Uses hardware impedance if available, falls back to
        heuristic estimation based on signal amplitude variation.

        Returns:
            Dict mapping channel name to impedance in kOhm.
            Lower values indicate better electrode contact.
            Values: <25 = good, 25-100 = marginal, >100 = poor.

        Example::

            impedance = device.get_impedance()
            for ch, kohm in impedance.items():
                status = "good" if kohm < 25 else "poor"
                print(f"{ch}: {kohm:.0f} kOhm ({status})")
        """
        return self._dm.check_impedance()

    def export(self, filepath: str, format: str = "csv") -> None:
        """
        Export the most recent data to a file.

        Args:
            filepath: Output file path.
            format: Export format — "csv", "edf", or "json".

        Raises:
            ValueError: If unsupported format or no data available.

        Example::

            device.export("recording.csv")
            device.export("recording.edf", format="edf")
        """
        nfdf = self._dm.get_current_data(seconds=60.0)
        if nfdf.eeg.size == 0:
            raise ValueError("No data available to export")

        if format == "csv":
            nfdf.to_csv(filepath)
        elif format == "edf":
            nfdf.to_edf(filepath)
        elif format == "json":
            import json as _json
            with open(filepath, "w") as f:
                _json.dump(nfdf.to_dict(), f, indent=2)
        else:
            raise ValueError(f"Unsupported format '{format}'. Use: csv, edf, json")

    def add_marker(self, label: str, timestamp: float = None) -> None:
        """
        Insert an event marker into the BrainFlow data stream.

        This uses BrainFlow's marker channel to insert a trigger value.

        Args:
            label: Marker label. Converted to a numeric value for BrainFlow.
            timestamp: Ignored for local devices (BrainFlow uses its own timing).
        """
        if not self.is_streaming:
            raise RuntimeError("Device must be streaming to add markers")
        # BrainFlow insert_marker expects a numeric value
        # We use the hash of the label modulo 10000 to get a reproducible number
        marker_value = abs(hash(label)) % 10000 + 1
        self._dm._board.insert_marker(float(marker_value))

    def calibrate(
        self,
        user_id: str,
        rest_duration: float = 120.0,
        focus_duration: float = 120.0,
        window_seconds: float = 2.0,
    ) -> dict:
        """
        Run a calibration protocol on this device.

        Collects baseline EEG during rest and focus phases, then
        computes a per-user CalibrationProfile for z-score normalization.

        Args:
            user_id: User/subject identifier.
            rest_duration: Rest phase duration in seconds.
            focus_duration: Focus phase duration in seconds.
            window_seconds: Feature extraction window size.

        Returns:
            CalibrationProfile dict with rest_mean, rest_std,
            focus_mean, focus_std, and feature_names.

        Raises:
            RuntimeError: If the device is not streaming.

        Example::

            device.start_stream()
            profile = device.calibrate("subject_001", rest_duration=60, focus_duration=60)
            print(f"Features: {len(profile['feature_names'])}")
            print(f"Quality: {profile['quality_score']:.2f}")
        """
        if not self.is_streaming:
            raise RuntimeError("Device must be streaming to calibrate")

        from neuroflow.calibration import CalibrationProtocol, CalibrationSettings

        settings = CalibrationSettings(
            rest_duration_seconds=rest_duration,
            focus_duration_seconds=focus_duration,
        )

        protocol = CalibrationProtocol(
            device_manager=self._dm,
            settings=settings,
            sampling_rate=self._dm.sampling_rate,
        )

        profile = protocol.run(
            user_id=user_id,
            device_id=self.id,
            window_seconds=window_seconds,
        )

        return profile.to_dict()

    def disconnect(self) -> None:
        """Disconnect the device and release resources."""
        self._dm.disconnect()

    def __repr__(self) -> str:
        streaming = " [streaming]" if self.is_streaming else ""
        return (
            f"LocalDevice(id={self.id!r}, name={self.name!r}, "
            f"status={self.status}{streaming})"
        )

    def __enter__(self) -> "LocalDevice":
        return self

    def __exit__(self, *args: Any) -> None:
        if self.is_streaming:
            self.stop_stream()
        if self.is_connected:
            self.disconnect()
