"""MP3 encoding for audio recordings using ffmpeg.

Reused from meeting-noter.
"""

from __future__ import annotations

import re
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Optional, Tuple

import numpy as np

try:
    import imageio_ffmpeg
    FFMPEG_PATH = imageio_ffmpeg.get_ffmpeg_exe()
except ImportError:
    FFMPEG_PATH = "ffmpeg"


def _sanitize_filename(name: str, max_length: int = 50) -> str:
    name = name.replace(" ", "_")
    name = re.sub(r"[^\w\-]", "", name)
    if len(name) > max_length:
        name = name[:max_length]
    name = name.rstrip("_-")
    return name


class MP3Encoder:
    """Encodes audio data to MP3 format using ffmpeg."""

    def __init__(
        self,
        output_path: Path,
        sample_rate: int = 16000,
        channels: int = 1,
        bitrate: int = 128,
    ):
        self.output_path = output_path
        self.sample_rate = sample_rate
        self.channels = channels
        self.bitrate = bitrate
        self._process: Optional[subprocess.Popen] = None
        self._start_ffmpeg()

    def _start_ffmpeg(self):
        cmd = [
            FFMPEG_PATH,
            "-y",
            "-f", "s16le",
            "-ar", str(self.sample_rate),
            "-ac", str(self.channels),
            "-i", "pipe:0",
            "-codec:a", "libmp3lame",
            "-b:a", f"{self.bitrate}k",
            "-f", "mp3",
            str(self.output_path),
        ]
        self._process = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )

    def encode_chunk(self, audio: np.ndarray) -> bytes:
        if self._process is None or self._process.stdin is None:
            return b""
        int_data = (audio * 32767).astype(np.int16)
        try:
            self._process.stdin.write(int_data.tobytes())
        except BrokenPipeError:
            pass
        return b""

    def finalize(self) -> bytes:
        if self._process is not None:
            try:
                if self._process.stdin is not None:
                    self._process.stdin.close()
            except Exception:
                pass
            try:
                self._process.wait(timeout=5.0)
            except subprocess.TimeoutExpired:
                self._process.kill()
                self._process.wait()
            self._process = None
        return b""


class RecordingSession:
    """Manages a single recording session (one meeting)."""

    def __init__(
        self,
        output_dir: Path,
        sample_rate: int = 16000,
        channels: int = 1,
        meeting_name: Optional[str] = None,
        start_time: Optional[datetime] = None,
    ):
        self.output_dir = output_dir
        self.sample_rate = sample_rate
        self.channels = channels
        self.meeting_name = meeting_name
        self._initial_start_time = start_time or datetime.now()
        self.encoder: Optional[MP3Encoder] = None
        self.filepath: Optional[Path] = None
        self.start_time: Optional[datetime] = None
        self.total_samples = 0

    def start(self) -> Path:
        self.start_time = self._initial_start_time
        self.output_dir.mkdir(parents=True, exist_ok=True)

        timestamp = self.start_time.strftime("%Y-%m-%d_%H%M%S")
        if self.meeting_name:
            sanitized = _sanitize_filename(self.meeting_name)
            filename = f"{timestamp}_{sanitized}.mp3"
        else:
            filename = f"{timestamp}.mp3"
        self.filepath = self.output_dir / filename

        self.encoder = MP3Encoder(
            output_path=self.filepath,
            sample_rate=self.sample_rate,
            channels=self.channels,
        )
        self.total_samples = 0
        return self.filepath

    def write(self, audio: np.ndarray):
        if self.encoder is None:
            raise RuntimeError("Recording session not started")
        self.encoder.encode_chunk(audio)
        self.total_samples += len(audio)

    def stop(self) -> Tuple[Optional[Path], float]:
        duration = 0.0
        filepath = self.filepath

        if self.encoder:
            self.encoder.finalize()
            duration = self.total_samples / self.sample_rate
            if duration < 5.0 and filepath and filepath.exists():
                filepath.unlink()
                filepath = None

        self.encoder = None
        self.filepath = None
        self.start_time = None
        self.total_samples = 0
        return filepath, duration

    @property
    def is_active(self) -> bool:
        return self.encoder is not None

    @property
    def duration(self) -> float:
        if self.sample_rate > 0:
            return self.total_samples / self.sample_rate
        return 0.0
