"""
neuroflow.client -- HTTP/WebSocket client for the NeuroFlow API server.

Provides the main Client class that connects to a running NeuroFlow
API server over HTTP (for commands) and WebSocket (for real-time
streaming). Supports both synchronous and asynchronous usage patterns.

Usage (synchronous)::

    import neuroflow

    client = neuroflow.Client(api_url="http://localhost:8000", api_key="nf_xxx")
    devices = client.discover()
    session = client.start_session(devices[0].id, pipeline="meditation")

    for data in session.stream(metrics=["focus", "alpha_power"]):
        print(data.features)

    session.stop()

Usage (async)::

    async with neuroflow.AsyncClient(api_url="http://localhost:8000") as client:
        devices = await client.discover()
        session = await client.start_session(devices[0].id)
        async for data in session.astream():
            print(data)
        await session.stop()
"""

from __future__ import annotations

from typing import Any, Optional

from neuroflow.device import RemoteDevice
from neuroflow.models import (
    Device as DeviceModel,
    DeviceStatus,
    Features,
    PipelineInfo,
    Session as SessionModel,
    SessionData,
    SessionStatus,
    SystemStatus,
)
from neuroflow.session import SessionHandle


class NeuroFlowAPIError(Exception):
    """
    Raised when the NeuroFlow API returns an error response.

    Attributes:
        status_code: HTTP status code from the server.
        detail: Error detail message from the server.
    """

    def __init__(self, status_code: int, detail: str) -> None:
        self.status_code = status_code
        self.detail = detail
        super().__init__(f"NeuroFlow API error {status_code}: {detail}")


class Client:
    """
    Synchronous HTTP client for the NeuroFlow API server.

    Connects to a running NeuroFlow server and provides methods for
    device discovery, session management, data retrieval, and real-time
    streaming. All network operations use httpx under the hood.

    Args:
        api_url: Base URL of the NeuroFlow API server.
        api_key: API key for authentication (optional if auth is disabled).
        timeout: Default request timeout in seconds.

    Example::

        client = neuroflow.Client("http://localhost:8000", api_key="nf_xxx")
        devices = client.discover()
        print(f"Found {len(devices)} devices")

        session = client.start_session(devices[0].id)
        for data in session.stream():
            print(data)
        session.stop()
        client.close()
    """

    def __init__(
        self,
        api_url: str = "http://localhost:8000",
        api_key: Optional[str] = None,
        timeout: float = 30.0,
    ) -> None:
        try:
            import httpx
        except ImportError:
            raise ImportError(
                "The 'httpx' package is required for the NeuroFlow client. "
                "Install it with: pip install neuroflow"
            )

        self._api_url = api_url.rstrip("/")
        self._api_key = api_key
        self._timeout = timeout

        headers = {"Content-Type": "application/json"}
        if api_key:
            headers["X-API-Key"] = api_key

        self._http = httpx.Client(
            base_url=self._api_url,
            headers=headers,
            timeout=timeout,
        )

    # ── Internal HTTP helpers ─────────────────────────────────

    def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        """Make an HTTP request and return the parsed JSON response."""
        response = self._http.request(method, path, **kwargs)
        if response.status_code >= 400:
            detail = ""
            try:
                body = response.json()
                detail = body.get("detail", str(body))
            except Exception:
                detail = response.text
            raise NeuroFlowAPIError(response.status_code, detail)
        if response.status_code == 204:
            return None
        return response.json()

    def _get(self, path: str, **kwargs: Any) -> Any:
        """HTTP GET request."""
        return self._request("GET", path, **kwargs)

    def _post(self, path: str, **kwargs: Any) -> Any:
        """HTTP POST request."""
        return self._request("POST", path, **kwargs)

    def _delete(self, path: str, **kwargs: Any) -> Any:
        """HTTP DELETE request."""
        return self._request("DELETE", path, **kwargs)

    # ── System ────────────────────────────────────────────────

    def status(self) -> SystemStatus:
        """
        Get the NeuroFlow server system status.

        Returns:
            SystemStatus with server health and activity information.
        """
        data = self._get("/status")
        return SystemStatus(**data)

    def health(self) -> bool:
        """
        Check if the server is reachable and healthy.

        Returns:
            True if the server responds successfully, False otherwise.
        """
        try:
            self._get("/health")
            return True
        except Exception:
            return False

    # ── Device Management ─────────────────────────────────────

    def discover(self) -> list[RemoteDevice]:
        """
        List all devices currently connected to the server.

        Returns:
            List of RemoteDevice objects for each connected device.

        Example::

            devices = client.discover()
            for device in devices:
                print(f"{device.name} ({device.status})")
        """
        data = self._get("/devices")
        devices = []
        for item in data:
            info = DeviceModel(
                device_id=item["device_id"],
                name=item.get("name", ""),
                manufacturer=item.get("manufacturer", ""),
                board_id=item.get("board_id", -1),
                status=DeviceStatus(item.get("status", "disconnected")),
                sampling_rate=item.get("sampling_rate", 0),
                num_channels=item.get("num_channels", 0),
                connection_type=item.get("connection_type", ""),
                is_streaming=item.get("is_streaming", False),
                total_samples=item.get("total_samples", 0),
                signal_quality=item.get("signal_quality", 0.0),
                uptime_seconds=item.get("uptime_seconds", 0.0),
            )
            devices.append(RemoteDevice(client=self, info=info))
        return devices

    def connect(
        self,
        board_id: int = -1,
        params: Optional[dict[str, Any]] = None,
        auto_reconnect: bool = True,
    ) -> RemoteDevice:
        """
        Connect a new device to the server.

        Args:
            board_id: BrainFlow board identifier (-1 for synthetic).
            params: Additional BrainFlow connection parameters.
            auto_reconnect: Whether to automatically reconnect on failure.

        Returns:
            RemoteDevice for the newly connected device.

        Example::

            # Connect a synthetic device for testing
            device = client.connect(board_id=-1)

            # Connect a Muse S via BLE
            device = client.connect(board_id=22, params={"mac_address": "XX:XX:XX"})
        """
        body = {
            "board_id": board_id,
            "params": params or {},
            "auto_reconnect": auto_reconnect,
        }
        data = self._post("/devices/connect", json=body)
        device_id = data["device_id"]

        # Fetch full device info
        info = self._get_device_info(device_id)
        return RemoteDevice(client=self, info=info)

    def _get_device_info(self, device_id: str) -> DeviceModel:
        """Fetch device info from the server."""
        data = self._get(f"/devices/{device_id}")
        return DeviceModel(
            device_id=data["device_id"],
            name=data.get("name", ""),
            manufacturer=data.get("manufacturer", ""),
            board_id=data.get("board_id", -1),
            status=DeviceStatus(data.get("status", "disconnected")),
            sampling_rate=data.get("sampling_rate", 0),
            num_channels=data.get("num_channels", 0),
            connection_type=data.get("connection_type", ""),
            is_streaming=data.get("is_streaming", False),
            total_samples=data.get("total_samples", 0),
            signal_quality=data.get("signal_quality", 0.0),
            uptime_seconds=data.get("uptime_seconds", 0.0),
        )

    def _start_device_stream(self, device_id: str) -> dict[str, str]:
        """Start streaming on a device."""
        return self._post(f"/devices/{device_id}/stream/start")

    def _stop_device_stream(self, device_id: str) -> dict[str, str]:
        """Stop streaming on a device."""
        return self._post(f"/devices/{device_id}/stream/stop")

    def _disconnect_device(self, device_id: str) -> dict[str, str]:
        """Disconnect a device."""
        return self._post(f"/devices/{device_id}/disconnect")

    # ── Session Management ────────────────────────────────────

    def start_session(
        self,
        device_id: str,
        name: str = "",
        subject_id: str = "",
        pipeline: str = "",
        pipeline_config: Optional[dict[str, Any]] = None,
        notes: str = "",
        tags: Optional[list[str]] = None,
    ) -> SessionHandle:
        """
        Start a new recording session on a device.

        Creates a session that captures EEG data from the specified device.
        Optionally applies a processing pipeline for real-time feature
        extraction.

        Args:
            device_id: Identifier of the device to record from.
            name: Human-readable session name.
            subject_id: Optional test subject identifier.
            pipeline: Processing pipeline preset name (e.g. "meditation",
                "focus_detection", "eeg_standard").
            pipeline_config: Custom pipeline configuration dict (overrides preset).
            notes: Free-form session notes.
            tags: List of categorization tags.

        Returns:
            SessionHandle for managing the active session.

        Example::

            session = client.start_session(
                device_id=device.id,
                pipeline="meditation",
                name="Morning meditation",
                tags=["meditation", "morning"],
            )
        """
        body = {
            "device_id": device_id,
            "name": name,
            "subject_id": subject_id,
            "pipeline_name": pipeline,
            "pipeline_config": pipeline_config or {},
            "notes": notes,
            "tags": tags or [],
        }
        data = self._post("/sessions", json=body)
        session_info = self._parse_session(data)
        return SessionHandle(client=self, session_info=session_info)

    def list_sessions(self, limit: int = 50) -> list[SessionModel]:
        """
        List all sessions on the server.

        Args:
            limit: Maximum number of sessions to return (1-500).

        Returns:
            List of Session models ordered by most recent first.
        """
        data = self._get("/sessions", params={"limit": limit})
        return [self._parse_session(item) for item in data]

    def get_session(self, session_id: str) -> SessionHandle:
        """
        Get a handle to an existing session.

        Args:
            session_id: Unique session identifier.

        Returns:
            SessionHandle for the specified session.
        """
        info = self._get_session_info(session_id)
        return SessionHandle(client=self, session_info=info)

    def _get_session_info(self, session_id: str) -> SessionModel:
        """Fetch session info from the server."""
        data = self._get(f"/sessions/{session_id}")
        return self._parse_session(data)

    def _stop_session(self, session_id: str) -> SessionModel:
        """Stop a recording session."""
        data = self._post(f"/sessions/{session_id}/stop")
        return self._parse_session(data)

    def _get_session_data(self, session_id: str) -> SessionData:
        """Fetch complete session data."""
        data = self._get(f"/sessions/{session_id}/data")
        return SessionData(
            session_id=data.get("session_id", session_id),
            sampling_rate=data.get("sampling_rate", 250),
            num_channels=data.get("num_channels", 0),
            num_samples=data.get("num_samples", 0),
            channel_names=data.get("channel_names", []),
            duration_seconds=data.get("duration_seconds", 0.0),
            eeg_data=data.get("eeg_data", []),
            timestamps=data.get("timestamps", []),
            features=data.get("features", {}),
            quality_scores=data.get("quality_scores", []),
        )

    def _get_session_features(self, session_id: str) -> dict[str, Any]:
        """Fetch session features."""
        return self._get(f"/sessions/{session_id}/features")

    def _delete_session(self, session_id: str) -> None:
        """Delete a session."""
        self._delete(f"/sessions/{session_id}")

    @staticmethod
    def _parse_session(data: dict[str, Any]) -> SessionModel:
        """Parse a session dict into a Session model."""
        return SessionModel(
            session_id=data.get("session_id", ""),
            device_id=data.get("device_id", ""),
            name=data.get("name", ""),
            status=SessionStatus(data.get("status", "created")),
            subject_id=data.get("subject_id", ""),
            pipeline_name=data.get("pipeline_name", ""),
            started_at=data.get("started_at"),
            stopped_at=data.get("stopped_at"),
            duration_seconds=data.get("duration_seconds", 0.0),
            total_samples=data.get("total_samples", 0),
            notes=data.get("notes", ""),
            tags=data.get("tags", []),
        )

    # ── Device (public wrappers) ─────────────────────────────────

    def get_device(self, device_id: str) -> DeviceModel:
        """
        Get device information by ID.

        Args:
            device_id: Unique device identifier.

        Returns:
            Device model with current device state.
        """
        return self._get_device_info(device_id)

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

        Args:
            device_id: Device to disconnect.

        Returns:
            Server response dict.
        """
        return self._disconnect_device(device_id)

    def start_device_stream(self, device_id: str) -> dict[str, str]:
        """
        Start data streaming on a device.

        Args:
            device_id: Device to start streaming.

        Returns:
            Server response dict.
        """
        return self._start_device_stream(device_id)

    def stop_device_stream(self, device_id: str) -> dict[str, str]:
        """
        Stop data streaming on a device.

        Args:
            device_id: Device to stop streaming.

        Returns:
            Server response dict.
        """
        return self._stop_device_stream(device_id)

    # ── Session (public wrappers) ─────────────────────────────────

    def stop_session(self, session_id: str) -> SessionModel:
        """
        Stop a recording session.

        Args:
            session_id: Session to stop.

        Returns:
            Updated Session model with final status.
        """
        return self._stop_session(session_id)

    def get_session_data(self, session_id: str) -> SessionData:
        """
        Retrieve complete recorded data for a session.

        Args:
            session_id: Session to retrieve data for.

        Returns:
            SessionData with EEG data, timestamps, and features.
        """
        return self._get_session_data(session_id)

    def get_session_features(self, session_id: str) -> dict[str, Any]:
        """
        Retrieve extracted features for a session.

        Args:
            session_id: Session to retrieve features for.

        Returns:
            Dict with feature names and values.
        """
        return self._get_session_features(session_id)

    def delete_session(self, session_id: str) -> None:
        """
        Delete a session and its data.

        Args:
            session_id: Session to delete.
        """
        self._delete_session(session_id)

    # ── Export ──────────────────────────────────────────────────

    def _export_session(self, session_id: str, format: str) -> bytes:
        """Download session data in the specified format."""
        response = self._http.request(
            "GET",
            f"/sessions/{session_id}/export",
            params={"format": format},
        )
        if response.status_code >= 400:
            detail = ""
            try:
                body = response.json()
                detail = body.get("detail", str(body))
            except Exception:
                detail = response.text
            raise NeuroFlowAPIError(response.status_code, detail)
        return response.content

    def export_session(
        self,
        session_id: str,
        format: str = "csv",
        filepath: str = "",
    ) -> bytes:
        """
        Export session data as a file.

        Args:
            session_id: Session to export.
            format: Export format — "csv", "edf", or "json".
            filepath: Optional path to save the file directly.
                If empty, returns the raw bytes.

        Returns:
            Raw bytes of the exported file.

        Example::

            # Save to file
            client.export_session("sess_001", format="csv", filepath="data.csv")

            # Or get bytes
            csv_bytes = client.export_session("sess_001", format="csv")
        """
        data = self._export_session(session_id, format)
        if filepath:
            with open(filepath, "wb") as f:
                f.write(data)
        return data

    # ── Markers / Events ───────────────────────────────────────

    def _add_marker(
        self,
        session_id: str,
        label: str,
        timestamp: float = None,
        metadata: dict = None,
    ) -> dict:
        """Add an event marker to a session."""
        body: dict = {"label": label}
        if timestamp is not None:
            body["timestamp"] = timestamp
        if metadata:
            body["metadata"] = metadata
        return self._post(f"/sessions/{session_id}/markers", json=body)

    def _get_markers(self, session_id: str) -> list[dict]:
        """Get all markers for a session."""
        return self._get(f"/sessions/{session_id}/markers")

    def add_marker(
        self,
        session_id: str,
        label: str,
        timestamp: float = None,
        metadata: dict = None,
    ) -> dict:
        """
        Add an event marker to a session.

        Markers are used for time-locked analysis (ERP, stimulus-response).

        Args:
            session_id: Session to add the marker to.
            label: Marker label (e.g. "stimulus_onset").
            timestamp: Unix timestamp. Defaults to server time.
            metadata: Optional metadata dict.

        Returns:
            Marker info dict with marker_id, label, and timestamp.
        """
        return self._add_marker(session_id, label, timestamp, metadata)

    def get_markers(self, session_id: str) -> list[dict]:
        """
        Get all event markers for a session.

        Args:
            session_id: Session to query.

        Returns:
            List of marker dicts sorted by timestamp.
        """
        return self._get_markers(session_id)

    # ── Impedance ──────────────────────────────────────────────

    def get_impedance(self, device_id: str) -> dict:
        """
        Get electrode impedance measurements for a device.

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

        Args:
            device_id: Device to measure.

        Returns:
            Dict with 'device_id', 'channels' (name -> kOhm),
            'timestamp', and 'quality_summary'.
        """
        return self._get(f"/devices/{device_id}/impedance")

    # ── Calibration ────────────────────────────────────────────

    def start_calibration(
        self,
        device_id: str,
        user_id: str,
        rest_duration: float = 120.0,
        focus_duration: float = 120.0,
        min_quality: float = 0.3,
    ) -> dict:
        """
        Start a calibration session on the server.

        The device must be streaming. Begins the rest phase.

        Args:
            device_id: Device to calibrate with.
            user_id: User/subject identifier.
            rest_duration: Rest phase duration in seconds.
            focus_duration: Focus phase duration in seconds.
            min_quality: Minimum signal quality (0.0–1.0).

        Returns:
            Calibration status dict with calibration_id and progress.
        """
        return self._post("/calibrations", json={
            "device_id": device_id,
            "user_id": user_id,
            "rest_duration_seconds": rest_duration,
            "focus_duration_seconds": focus_duration,
            "min_quality_score": min_quality,
        })

    def calibration_collect(self, calibration_id: str) -> dict:
        """Collect a data window for the current calibration phase."""
        return self._post(f"/calibrations/{calibration_id}/collect")

    def calibration_switch_phase(self, calibration_id: str, phase: str) -> dict:
        """
        Switch the calibration to a different phase.

        Args:
            calibration_id: Active calibration ID.
            phase: "rest" or "focus".
        """
        return self._post(f"/calibrations/{calibration_id}/phase/{phase}")

    def complete_calibration(self, calibration_id: str) -> dict:
        """
        Complete calibration and compute the baseline profile.

        Args:
            calibration_id: Active calibration ID.

        Returns:
            CalibrationProfile dict with rest_mean, rest_std, etc.
        """
        return self._post(f"/calibrations/{calibration_id}/complete")

    def get_calibration(self, user_id: str) -> dict:
        """
        Get the most recent calibration profile for a user.

        Args:
            user_id: User/subject identifier.

        Returns:
            CalibrationProfile dict.
        """
        return self._get(f"/calibrations/user/{user_id}")

    def get_calibration_profile(self, calibration_id: str) -> dict:
        """Get a specific calibration profile by ID."""
        return self._get(f"/calibrations/profile/{calibration_id}")

    def delete_calibration(self, calibration_id: str) -> dict:
        """Delete a calibration profile."""
        return self._delete(f"/calibrations/{calibration_id}")

    # ── Calibration (public wrappers) ─────────────────────────

    def get_calibration_status(self, calibration_id: str) -> dict:
        """
        Get progress of an active calibration session.

        Args:
            calibration_id: Active calibration ID.

        Returns:
            Progress dict with phase, windows collected, etc.
        """
        return self._get(f"/calibrations/{calibration_id}/status")

    # ── Pipeline Management ───────────────────────────────────

    def get_pipeline(self, name: str) -> dict:
        """
        Get a specific pipeline configuration.

        Args:
            name: Pipeline name (e.g. "eeg_standard", "meditation").

        Returns:
            Pipeline configuration dict with steps and feature extractors.
        """
        return self._get(f"/pipelines/{name}")

    def list_pipelines(self) -> list[PipelineInfo]:
        """
        List available processing pipelines on the server.

        Returns:
            List of PipelineInfo objects with pipeline details.
        """
        data = self._get("/pipelines")
        return [
            PipelineInfo(
                name=item.get("name", ""),
                steps=item.get("steps", []),
                feature_extractors=item.get("feature_extractors", item.get("features", [])),
                is_preset=item.get("is_preset", False),
                description=item.get("description", ""),
            )
            for item in data
        ]

    # ── Processing ────────────────────────────────────────────

    def process(
        self,
        data: Optional[list[list[float]]] = None,
        session_id: str = "",
        pipeline: str = "eeg_standard",
        sampling_rate: int = 250,
    ) -> dict[str, Any]:
        """
        Process EEG data through a pipeline (one-shot).

        Either provide raw data directly or reference a stored session.

        Args:
            data: Raw EEG data as [channels][samples] (optional).
            session_id: Session to process (alternative to raw data).
            pipeline: Pipeline name to use for processing.
            sampling_rate: Sampling rate of the data in Hz.

        Returns:
            Processing result dict with features and timing.
        """
        body: dict[str, Any] = {
            "pipeline_name": pipeline,
            "sampling_rate": sampling_rate,
        }
        if data is not None:
            body["data"] = data
        if session_id:
            body["session_id"] = session_id

        return self._post("/process", json=body)

    # ── Models & Training ───────────────────────────────────────

    def list_models(self, status: Optional[str] = None) -> list[dict]:
        """
        List registered models on the server.

        Args:
            status: Optional filter by model status.

        Returns:
            List of model metadata dicts.
        """
        params = {"status": status} if status else {}
        return self._get("/models", params=params)

    def load_model(self, name: str, version: Optional[str] = None) -> dict:
        """
        Get model metadata from the registry.

        Args:
            name: Model name.
            version: Optional model version.

        Returns:
            Model metadata dict.
        """
        url = f"/models/{name}"
        if version:
            url += f"?version={version}"
        return self._get(url)

    def predict(
        self,
        model_name: str,
        features: Any,
        session_id: Optional[str] = None,
    ) -> dict:
        """
        Run inference with a registered model.

        Args:
            model_name: Name of the model in the registry.
            features: Feature dict or list of feature values.
            session_id: Optional session to extract features from.

        Returns:
            Prediction dict with label, confidence, and probabilities.
        """
        body: dict[str, Any] = {
            "model_name": model_name,
            "features": features,
        }
        if session_id:
            body["session_id"] = session_id
        return self._post(f"/models/{model_name}/predict", json=body)

    def quick_train(
        self,
        X: list,
        y: list,
        class_names: Optional[list[str]] = None,
        architecture: str = "auto",
        name: str = "model",
    ) -> dict:
        """
        Train a model on feature data.

        Args:
            X: Feature matrix as nested list [n_samples][n_features].
            y: Labels as list [n_samples].
            class_names: Optional class name list.
            architecture: Model architecture (or "auto").
            name: Model name for the registry.

        Returns:
            Training result dict with metrics, model name, and success flag.
        """
        return self._post("/models/train", json={
            "X": X,
            "y": y,
            "class_names": class_names,
            "architecture": architecture,
            "name": name,
        })

    # ── Lifecycle ─────────────────────────────────────────────

    def close(self) -> None:
        """Close the HTTP client and release resources."""
        self._http.close()

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

    def __exit__(self, *args: Any) -> None:
        self.close()

    def __repr__(self) -> str:
        auth = " (authenticated)" if self._api_key else ""
        return f"neuroflow.Client(url={self._api_url!r}{auth})"


# ──────────────────────────────────────────────────────────────
# Async Client
# ──────────────────────────────────────────────────────────────

class AsyncClient:
    """
    Asynchronous HTTP client for the NeuroFlow API server.

    Provides the same functionality as Client but with async/await
    methods. Uses httpx.AsyncClient under the hood.

    Args:
        api_url: Base URL of the NeuroFlow API server.
        api_key: API key for authentication.
        timeout: Default request timeout in seconds.

    Example::

        async with neuroflow.AsyncClient("http://localhost:8000") as client:
            devices = await client.discover()
            device = await client.connect(board_id=-1)
            print(device.name)
    """

    def __init__(
        self,
        api_url: str = "http://localhost:8000",
        api_key: Optional[str] = None,
        timeout: float = 30.0,
    ) -> None:
        try:
            import httpx
        except ImportError:
            raise ImportError(
                "The 'httpx' package is required for the NeuroFlow client. "
                "Install it with: pip install neuroflow"
            )

        self._api_url = api_url.rstrip("/")
        self._api_key = api_key
        self._timeout = timeout

        headers = {"Content-Type": "application/json"}
        if api_key:
            headers["X-API-Key"] = api_key

        self._http = httpx.AsyncClient(
            base_url=self._api_url,
            headers=headers,
            timeout=timeout,
        )

    async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        """Make an async HTTP request and return parsed JSON."""
        response = await self._http.request(method, path, **kwargs)
        if response.status_code >= 400:
            detail = ""
            try:
                body = response.json()
                detail = body.get("detail", str(body))
            except Exception:
                detail = response.text
            raise NeuroFlowAPIError(response.status_code, detail)
        if response.status_code == 204:
            return None
        return response.json()

    async def _get(self, path: str, **kwargs: Any) -> Any:
        return await self._request("GET", path, **kwargs)

    async def _post(self, path: str, **kwargs: Any) -> Any:
        return await self._request("POST", path, **kwargs)

    async def _delete(self, path: str, **kwargs: Any) -> Any:
        return await self._request("DELETE", path, **kwargs)

    async def status(self) -> SystemStatus:
        """Get server system status."""
        data = await self._get("/status")
        return SystemStatus(**data)

    async def health(self) -> bool:
        """Check if the server is reachable."""
        try:
            await self._get("/health")
            return True
        except Exception:
            return False

    async def discover(self) -> list[DeviceModel]:
        """List all connected devices."""
        data = await self._get("/devices")
        return [
            DeviceModel(
                device_id=item["device_id"],
                name=item.get("name", ""),
                manufacturer=item.get("manufacturer", ""),
                board_id=item.get("board_id", -1),
                status=DeviceStatus(item.get("status", "disconnected")),
                sampling_rate=item.get("sampling_rate", 0),
                num_channels=item.get("num_channels", 0),
                connection_type=item.get("connection_type", ""),
                is_streaming=item.get("is_streaming", False),
                total_samples=item.get("total_samples", 0),
            )
            for item in data
        ]

    async def connect(
        self,
        board_id: int = -1,
        params: Optional[dict[str, Any]] = None,
        auto_reconnect: bool = True,
    ) -> DeviceModel:
        """Connect a new device and return its info."""
        body = {
            "board_id": board_id,
            "params": params or {},
            "auto_reconnect": auto_reconnect,
        }
        data = await self._post("/devices/connect", json=body)
        device_id = data["device_id"]
        info_data = await self._get(f"/devices/{device_id}")
        return DeviceModel(
            device_id=info_data["device_id"],
            name=info_data.get("name", ""),
            manufacturer=info_data.get("manufacturer", ""),
            board_id=info_data.get("board_id", -1),
            status=DeviceStatus(info_data.get("status", "disconnected")),
            sampling_rate=info_data.get("sampling_rate", 0),
            num_channels=info_data.get("num_channels", 0),
            is_streaming=info_data.get("is_streaming", False),
        )

    async def start_session(
        self,
        device_id: str,
        name: str = "",
        pipeline: str = "",
        **kwargs: Any,
    ) -> SessionModel:
        """Start a recording session."""
        body = {
            "device_id": device_id,
            "name": name,
            "pipeline_name": pipeline,
            "subject_id": kwargs.get("subject_id", ""),
            "pipeline_config": kwargs.get("pipeline_config", {}),
            "notes": kwargs.get("notes", ""),
            "tags": kwargs.get("tags", []),
        }
        data = await self._post("/sessions", json=body)
        return Client._parse_session(data)

    async def stop_session(self, session_id: str) -> SessionModel:
        """Stop a recording session."""
        data = await self._post(f"/sessions/{session_id}/stop")
        return Client._parse_session(data)

    async def get_session_data(self, session_id: str) -> SessionData:
        """Fetch complete session data."""
        data = await self._get(f"/sessions/{session_id}/data")
        return SessionData(
            session_id=data.get("session_id", session_id),
            sampling_rate=data.get("sampling_rate", 250),
            num_channels=data.get("num_channels", 0),
            num_samples=data.get("num_samples", 0),
            eeg_data=data.get("eeg_data", []),
            timestamps=data.get("timestamps", []),
            features=data.get("features", {}),
        )

    async def close(self) -> None:
        """Close the async HTTP client."""
        await self._http.aclose()

    async def __aenter__(self) -> "AsyncClient":
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()

    def __repr__(self) -> str:
        auth = " (authenticated)" if self._api_key else ""
        return f"neuroflow.AsyncClient(url={self._api_url!r}{auth})"
