"""Efficient binary file access with memory mapping, caching, and thread safety."""

from __future__ import annotations

import mmap
import threading
from pathlib import Path
from typing import TYPE_CHECKING, Any

import numpy as np

if TYPE_CHECKING:
    from os import PathLike


class BinaryFile:
    """Memory-efficient, thread-safe binary file access layer.

    Provides efficient access to binary files of any size using memory mapping
    and caching. Designed to handle multi-GB files with minimal memory footprint.
    All operations are protected by an RLock for thread safety.

    Example:
        >>> bf = BinaryFile("large_file.bin")
        >>> data = bf.read_bytes(0, 1024)  # Read first 1KB
        >>> samples = bf.sample_locations(n_samples=5)  # Sample 5 locations
        >>> bf.close()
    """

    def __init__(self, path: str | PathLike[str]) -> None:
        """Initialize binary file access.

        Args:
            path: Path to binary file.
        """
        self.path = Path(path)
        self.size = self.path.stat().st_size
        self._file_handle: Any | None = None
        self._mmap: mmap.mmap | None = None
        self._lock = threading.RLock()

    def _ensure_open(self) -> None:
        """Ensure file is open and memory mapped.

        Thread-safe. Cleans up partial state on failure to prevent
        resource leaks.
        """
        with self._lock:
            if self._mmap is None:
                try:
                    self._file_handle = open(self.path, "rb")  # noqa: SIM115
                    if self.size > 0:
                        self._mmap = mmap.mmap(
                            self._file_handle.fileno(), 0, access=mmap.ACCESS_READ
                        )
                except Exception:
                    if self._file_handle is not None:
                        self._file_handle.close()
                        self._file_handle = None
                    raise

    def read_bytes(self, offset: int, length: int) -> bytes:
        """Read bytes from file at given offset.

        Args:
            offset: Byte offset to start reading.
            length: Number of bytes to read.

        Returns:
            Bytes read from file.

        Example:
            >>> bf = BinaryFile("data.bin")
            >>> header = bf.read_bytes(0, 16)
        """
        if offset + length > self.size:
            length = max(0, self.size - offset)

        if length <= 0:
            return b""

        self._ensure_open()

        with self._lock:
            if self._mmap is not None:
                return bytes(self._mmap[offset : offset + length])

        return b""

    def read_regions(self, regions: list[tuple[int, int]]) -> list[bytes]:
        """Read multiple regions efficiently.

        Args:
            regions: List of (offset, length) tuples.

        Returns:
            List of bytes for each region.

        Example:
            >>> bf = BinaryFile("data.bin")
            >>> regions = [(0, 16), (1000, 16), (2000, 16)]
            >>> data = bf.read_regions(regions)
        """
        return [self.read_bytes(offset, length) for offset, length in regions]

    def sample_locations(self, n_samples: int = 5, sample_size: int = 8192) -> list[bytes]:
        """Sample data from multiple file locations.

        Samples evenly distributed throughout the file for robust analysis.

        Args:
            n_samples: Number of locations to sample.
            sample_size: Bytes to read at each location.

        Returns:
            List of byte samples from different file locations.

        Example:
            >>> bf = BinaryFile("data.bin")
            >>> samples = bf.sample_locations(n_samples=5, sample_size=4096)
        """
        if self.size < sample_size:
            return [self.read_bytes(0, self.size)]

        # Calculate evenly-spaced locations
        locations = np.linspace(0, self.size - sample_size, n_samples, dtype=int)

        return [self.read_bytes(int(loc), sample_size) for loc in locations]

    def read_array(
        self, offset: int, count: int, dtype: str | np.dtype[Any]
    ) -> np.ndarray[Any, Any]:
        """Read data as numpy array.

        Args:
            offset: Byte offset to start reading.
            count: Number of elements to read (-1 for all).
            dtype: NumPy dtype for interpretation.

        Returns:
            NumPy array of data.

        Example:
            >>> bf = BinaryFile("data.bin")
            >>> data = bf.read_array(0, 1000, dtype="uint16")
        """
        np_dtype = np.dtype(dtype)
        bytes_per_element = np_dtype.itemsize
        byte_offset = offset * bytes_per_element

        if count == -1:
            # Read all available data
            available_bytes = self.size - byte_offset
            count = available_bytes // bytes_per_element

        if count <= 0:
            return np.array([], dtype=np_dtype)

        bytes_to_read = count * bytes_per_element

        if byte_offset + bytes_to_read > self.size:
            bytes_to_read = self.size - byte_offset
            count = bytes_to_read // bytes_per_element

        data_bytes = self.read_bytes(byte_offset, bytes_to_read)
        return np.frombuffer(data_bytes, dtype=np_dtype, count=count)

    def close(self) -> None:
        """Close file and release resources."""
        with self._lock:
            if self._mmap is not None:
                self._mmap.close()
                self._mmap = None

            if self._file_handle is not None:
                self._file_handle.close()
                self._file_handle = None

    def __enter__(self) -> BinaryFile:
        """Context manager entry."""
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Context manager exit."""
        self.close()

    def __del__(self) -> None:
        """Destructor to ensure cleanup."""
        self.close()

    def __repr__(self) -> str:
        """String representation."""
        return f"BinaryFile('{self.path}', size={self.size:,} bytes)"
