"""Pre-allocated audio buffer for partial transcription.

Optimizes the partial transcription pipeline by avoiding repeated
list appends and np.concatenate allocations.
"""

from __future__ import annotations

from typing import Final

import numpy as np
from numpy.typing import NDArray


class PartialAudioBuffer:
    """Pre-allocated ring buffer for accumulating audio chunks.

    Replaces the pattern of appending to a list and concatenating,
    which causes many small allocations. Instead, uses a fixed-size
    numpy array with a write pointer.

    Performance characteristics:
    - append(): O(n) where n = chunk size (memcpy), no allocation
    - get_audio(): O(n) where n = accumulated samples (single copy)
    - clear(): O(1), just resets pointer
    - Memory: Fixed ~320KB for 5 seconds at 16kHz

    Thread safety: Not thread-safe. Designed for single-coroutine use
    per meeting in the async gRPC streaming context.
    """

    # Default buffer capacity (5 seconds should cover any partial window)
    DEFAULT_MAX_DURATION: Final[float] = 5.0

    def __init__(
        self,
        max_duration_seconds: float = DEFAULT_MAX_DURATION,
        sample_rate: int = 16000,
    ) -> None:
        """Initialize the buffer.

        Args:
            max_duration_seconds: Maximum audio duration to buffer.
            sample_rate: Audio sample rate in Hz.
        """
        self._sample_rate = sample_rate
        self._capacity = int(max_duration_seconds * sample_rate)
        self._buffer: NDArray[np.float32] = np.zeros(self._capacity, dtype=np.float32)
        self._write_pos: int = 0

    @property
    def sample_rate(self) -> int:
        """Get the configured sample rate."""
        return self._sample_rate

    @property
    def capacity_samples(self) -> int:
        """Get the buffer capacity in samples."""
        return self._capacity

    @property
    def samples_buffered(self) -> int:
        """Get the number of samples currently in the buffer."""
        return self._write_pos

    @property
    def duration_seconds(self) -> float:
        """Get the duration of buffered audio in seconds."""
        return self._write_pos / self._sample_rate

    @property
    def is_empty(self) -> bool:
        """Check if the buffer is empty."""
        return self._write_pos == 0

    def append(self, audio: NDArray[np.float32]) -> bool:
        """Append audio samples to the buffer.

        Args:
            audio: Audio samples to append (float32).

        Returns:
            True if samples were appended, False if buffer would overflow.
            On overflow, samples are silently dropped (caller should have
            read and cleared before this happens).
        """
        samples = len(audio)
        new_pos = self._write_pos + samples

        if new_pos > self._capacity:
            # Buffer overflow - this indicates a bug in the caller
            # (should have read and cleared within the partial cadence)
            # Drop the samples rather than corrupt memory
            return False

        # Copy samples into pre-allocated buffer (no allocation)
        self._buffer[self._write_pos : new_pos] = audio
        self._write_pos = new_pos
        return True

    def get_audio(self) -> NDArray[np.float32]:
        """Get a copy of the buffered audio.

        Returns:
            Copy of buffered audio samples. Returns empty array if buffer
            is empty. A copy is returned because the caller (ASR engine)
            may hold the reference while we continue appending.
        """
        if self._write_pos == 0:
            return np.array([], dtype=np.float32)
        # Return copy of the valid portion
        return self._buffer[: self._write_pos].copy()

    def get_audio_view(self) -> NDArray[np.float32]:
        """Get a view of the buffered audio (no copy).

        Returns:
            View into the buffer. WARNING: Only use if you will consume
            the data before the next append() or clear() call.
        """
        return self._buffer[: self._write_pos]

    def clear(self) -> None:
        """Clear the buffer.

        Resets the write pointer without zeroing memory (O(1)).
        Old data remains but is inaccessible and will be overwritten.
        """
        self._write_pos = 0

    def __len__(self) -> int:
        """Return number of samples buffered."""
        return self._write_pos

    def __bool__(self) -> bool:
        """Return True if buffer has samples."""
        return self._write_pos > 0
