"""Shared audio-silence sentence boundary detection.

This module intentionally has no GUI or web-service dependencies so the
desktop player, the pre-generation CLI, and the API worker use the same
algorithm.
"""

SILENCE_FLOOR_PERCENTILE = 10
SILENCE_THRESHOLD_FACTOR = 1.5
SILENCE_BRIDGE_GAP = 0.06


def detect_sentence_boundaries(path, min_silence=0.30, min_sentence=0.35):
    """Return sentence start times in seconds, or None when unavailable.

    Audio is resampled to mono 16 kHz, divided into 30 ms windows, and
    silence runs are converted into boundaries. This matches the original
    desktop player's behavior.
    """
    try:
        import av
        import numpy as np
    except ImportError:
        return None

    container = None
    try:
        container = av.open(str(path))
        audio = next((stream for stream in container.streams if stream.type == "audio"), None)
        if audio is None:
            return None

        sample_rate = 16_000
        resampler = av.AudioResampler(format="fltp", layout="mono", rate=sample_rate)
        window = 480
        frame_seconds = window / sample_rate
        energies = []
        buffer = []

        def push(data):
            buffer.append(data)
            total = sum(len(chunk) for chunk in buffer)
            if total < window:
                return
            array = np.concatenate(buffer)
            buffer.clear()
            complete = len(array) // window * window
            windows = array[:complete].reshape(-1, window)
            energies.extend((np.mean(windows * windows, axis=1) ** 0.5).tolist())
            if len(array) > complete:
                buffer.append(array[complete:])

        for packet in container.demux(audio):
            for frame in packet.decode():
                for output in resampler.resample(frame):
                    push(output.to_ndarray()[0])
        for output in resampler.resample(None):
            push(output.to_ndarray()[0])
        if buffer:
            push(np.zeros(window, dtype=np.float32))

        duration = container.duration
        if len(energies) < 3:
            return None

        energies = np.asarray(energies, dtype=np.float64)
        signal_floor = float(np.percentile(energies, 95))
        if signal_floor <= 0.0:
            return None
        noise_floor = float(np.percentile(energies, SILENCE_FLOOR_PERCENTILE))
        threshold = max(0.008, SILENCE_THRESHOLD_FACTOR * noise_floor)
        silence = energies < threshold

        bridge_frames = int(round(SILENCE_BRIDGE_GAP / frame_seconds))
        if bridge_frames > 0:
            bridged = silence.copy()
            run_start = None
            for index, is_silent in enumerate(silence):
                if not is_silent and run_start is None:
                    run_start = index
                elif is_silent and run_start is not None:
                    if run_start > 0 and index - run_start <= bridge_frames:
                        bridged[run_start:index] = True
                    run_start = None
            silence = bridged

        boundaries = [0.0]
        run_start = None
        for index, is_silent in enumerate(silence):
            if is_silent and run_start is None:
                run_start = index
            elif not is_silent and run_start is not None:
                if (index - run_start) * frame_seconds >= min_silence:
                    end_seconds = index * frame_seconds
                    if end_seconds - boundaries[-1] >= min_sentence:
                        boundaries.append(end_seconds)
                run_start = None
        if run_start is not None and (len(silence) - run_start) * frame_seconds >= min_silence:
            end_seconds = len(silence) * frame_seconds
            if end_seconds - boundaries[-1] >= min_sentence:
                boundaries.append(end_seconds)

        if duration and duration > 0:
            boundaries = [boundary for boundary in boundaries if boundary < duration / 1e6 - 0.1]
        return boundaries
    except Exception:
        return None
    finally:
        if container is not None:
            container.close()


__all__ = ["detect_sentence_boundaries"]
