# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Voice Skill - Echo, Voice of the Gods

Comprehensive voice capabilities:
- Text-to-speech (TTS) with multiple engines
- Speech-to-text (STT) via Whisper (local) or cloud APIs
- Real-time transcription streaming
- Audio file transcription (any format)
- Voice activity detection
- Speaker diarization (who said what)
- Meeting/interview transcription
- Voice memos with automatic summarization

Whisper Models (choose based on hardware):
- tiny: ~1GB RAM, fast, good for Pi 4
- base: ~1.5GB RAM, better accuracy
- small: ~2GB RAM, good balance
- medium: ~5GB RAM, very accurate
- large: ~10GB RAM, best accuracy (needs GPU)

Works offline on Raspberry Pi with tiny/base models.
"""

import json
import logging
import os
import queue
import tempfile
import threading
import wave
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Callable, Optional

logger = logging.getLogger(__name__)

# Import document reader extension
try:
    from .document_reader import (
        get_reading_status,
        install_piper,
        list_voices,
        read_document,
        stop_reading,
    )

    DOCUMENT_READER_AVAILABLE = True
except ImportError:
    DOCUMENT_READER_AVAILABLE = False
    logger.debug("Document reader not available")

# Storage paths - use centralized paths


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.paths import DATA_DIR, VOICE_DIR, ensure_dir

    AUDIO_DIR = VOICE_DIR / "audio"
    TRANSCRIPTS_DIR = VOICE_DIR / "transcripts"
except ImportError:
    DATA_DIR = _get_data_dir()
    AUDIO_DIR = DATA_DIR / "audio"
    TRANSCRIPTS_DIR = DATA_DIR / "transcripts"

    def ensure_dir(path):
        path.mkdir(parents=True, exist_ok=True)
        return path


VOICE_CONFIG_FILE = DATA_DIR / "voice_config.json"

ensure_dir(AUDIO_DIR)
ensure_dir(TRANSCRIPTS_DIR)

# Global state
_whisper_model = None
_whisper_model_name = None
_tts_engine = None
_recording_thread = None
_is_recording = False


@dataclass
class TranscriptSegment:
    """A segment of transcribed audio."""

    start: float  # seconds
    end: float
    text: str
    speaker: Optional[str] = None
    confidence: Optional[float] = None


@dataclass
class Transcript:
    """Full transcript with metadata."""

    segments: list
    text: str
    duration: float
    language: str
    created_at: str
    source_file: Optional[str] = None
    model_used: str = "whisper"


def _load_voice_config() -> dict:
    """Load voice configuration."""
    if VOICE_CONFIG_FILE.exists():
        try:
            return json.loads(VOICE_CONFIG_FILE.read_text())
        except (json.JSONDecodeError, IOError, OSError) as e:
            logger.warning(f"Failed to load voice config: {e}")
    return {
        "whisper_model": "tiny",  # Default for Pi
        "tts_engine": "auto",
        "tts_rate": 150,
        "tts_volume": 1.0,
        "tts_voice": None,
        "language": "en",
        "vad_threshold": 0.5,
        "auto_punctuate": True,
    }


def _save_voice_config(config: dict):
    """Save voice configuration."""
    VOICE_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
    VOICE_CONFIG_FILE.write_text(json.dumps(config, indent=2))


# ============================================================
# WHISPER TRANSCRIPTION
# ============================================================


def _get_whisper_model(model_name: str = None):
    """Load or get cached Whisper model."""
    global _whisper_model, _whisper_model_name

    config = _load_voice_config()
    model_name = model_name or config.get("whisper_model", "tiny")

    # Return cached if same model
    if _whisper_model is not None and _whisper_model_name == model_name:
        return _whisper_model

    try:
        import whisper

        logger.info(f"Loading Whisper model: {model_name}")
        _whisper_model = whisper.load_model(model_name)
        _whisper_model_name = model_name

        return _whisper_model

    except ImportError:
        from familiar.core.deps import VOICE_STT_PACKAGES, ensure_packages

        ok, _ = ensure_packages(VOICE_STT_PACKAGES)
        if ok:
            # Retry with faster-whisper (just installed)
            return _get_faster_whisper_model(model_name) or _get_whisper_model(model_name)
        raise RuntimeError("Voice transcription setup failed. Check server logs.")


def _get_faster_whisper_model(model_name: str = None):
    """Load faster-whisper model (more efficient, recommended)."""
    global _whisper_model, _whisper_model_name

    config = _load_voice_config()
    model_name = model_name or config.get("whisper_model", "tiny")

    if _whisper_model is not None and _whisper_model_name == f"faster-{model_name}":
        return _whisper_model

    try:
        from faster_whisper import WhisperModel

        # Detect compute type based on hardware
        compute_type = "int8"  # Good for CPU/Pi
        try:
            import torch

            if torch.cuda.is_available():
                compute_type = "float16"
        except ImportError as e:
            logger.debug("Optional dependency not available: %s", e)
        logger.info(f"Loading faster-whisper model: {model_name} ({compute_type})")
        _whisper_model = WhisperModel(model_name, compute_type=compute_type)
        _whisper_model_name = f"faster-{model_name}"

        return _whisper_model

    except ImportError:
        return None


def _transcribe_with_whisper(
    audio_path: str,
    model_name: str = None,
    language: str = None,
    task: str = "transcribe",  # or "translate" for translation to English
    word_timestamps: bool = False,
    initial_prompt: str = None,
) -> Transcript:
    """
    Transcribe audio file using Whisper.

    Args:
        audio_path: Path to audio file (any format ffmpeg supports)
        model_name: Whisper model size (tiny, base, small, medium, large)
        language: Language code (auto-detected if None)
        task: "transcribe" or "translate"
        word_timestamps: Include word-level timestamps
        initial_prompt: Context to help transcription

    Returns:
        Transcript object with segments and full text
    """
    config = _load_voice_config()
    language = language or config.get("language")

    # Try faster-whisper first
    faster_model = _get_faster_whisper_model(model_name)

    if faster_model is not None:
        return _transcribe_faster_whisper(
            faster_model, audio_path, language, task, word_timestamps, initial_prompt
        )

    # Fall back to standard whisper
    model = _get_whisper_model(model_name)

    options = {
        "task": task,
        "word_timestamps": word_timestamps,
    }

    if language and language != "auto":
        options["language"] = language

    if initial_prompt:
        options["initial_prompt"] = initial_prompt

    result = model.transcribe(audio_path, **options)

    # Build segments
    segments = []
    for seg in result.get("segments", []):
        segments.append(
            TranscriptSegment(
                start=seg["start"],
                end=seg["end"],
                text=seg["text"].strip(),
                confidence=seg.get("avg_logprob"),
            )
        )

    # Calculate duration
    duration = segments[-1].end if segments else 0

    return Transcript(
        segments=[asdict(s) for s in segments],
        text=result["text"].strip(),
        duration=duration,
        language=result.get("language", "en"),
        created_at=datetime.now().isoformat(),
        source_file=str(audio_path),
        model_used=f"whisper-{_whisper_model_name}",
    )


def _transcribe_faster_whisper(
    model, audio_path: str, language: str, task: str, word_timestamps: bool, initial_prompt: str
) -> Transcript:
    """Transcribe using faster-whisper (more efficient)."""

    options = {
        "task": task,
        "word_timestamps": word_timestamps,
        "vad_filter": True,  # Voice activity detection
    }

    if language and language != "auto":
        options["language"] = language

    if initial_prompt:
        options["initial_prompt"] = initial_prompt

    segments_gen, info = model.transcribe(audio_path, **options)

    segments = []
    full_text = []

    for seg in segments_gen:
        segments.append(
            TranscriptSegment(
                start=seg.start,
                end=seg.end,
                text=seg.text.strip(),
                confidence=seg.avg_logprob if hasattr(seg, "avg_logprob") else None,
            )
        )
        full_text.append(seg.text.strip())

    duration = segments[-1].end if segments else 0

    return Transcript(
        segments=[asdict(s) for s in segments],
        text=" ".join(full_text),
        duration=duration,
        language=info.language if hasattr(info, "language") else "en",
        created_at=datetime.now().isoformat(),
        source_file=str(audio_path),
        model_used=f"faster-whisper-{_whisper_model_name}",
    )


# ============================================================
# REAL-TIME TRANSCRIPTION
# ============================================================


class RealtimeTranscriber:
    """
    Real-time speech transcription using streaming audio.

    Usage:
        transcriber = RealtimeTranscriber()
        transcriber.start(callback=lambda text: print(text))
        # ... speak ...
        transcriber.stop()
        full_transcript = transcriber.get_transcript()
    """

    def __init__(self, model_name: str = "tiny", language: str = "en"):
        self.model_name = model_name
        self.language = language
        self.audio_queue = queue.Queue()
        self.transcript_segments = []
        self.is_running = False
        self._thread = None
        self._callback = None

        # Audio settings
        self.sample_rate = 16000
        self.chunk_duration = 3  # seconds per chunk
        self.overlap = 0.5  # overlap between chunks

    def start(self, callback: Callable[[str], None] = None):
        """Start real-time transcription."""
        try:
            import pyaudio  # noqa: F401
        except ImportError:
            from familiar.core.deps import PYAUDIO_PACKAGES, ensure_packages

            ok, _ = ensure_packages(PYAUDIO_PACKAGES)
            if not ok:
                raise RuntimeError(
                    "pyaudio not available. It requires the PortAudio system library — "
                    "on Debian/Ubuntu run: sudo apt install portaudio19-dev"
                )

        self._callback = callback
        self.is_running = True
        self.transcript_segments = []

        # Start audio capture thread
        self._audio_thread = threading.Thread(target=self._capture_audio)
        self._audio_thread.daemon = True
        self._audio_thread.start()

        # Start transcription thread
        self._thread = threading.Thread(target=self._transcribe_loop)
        self._thread.daemon = True
        self._thread.start()

        logger.info("Real-time transcription started")

    def stop(self) -> str:
        """Stop transcription and return full transcript."""
        self.is_running = False

        if self._thread:
            self._thread.join(timeout=5)

        return self.get_transcript()

    def get_transcript(self) -> str:
        """Get full transcript so far."""
        return " ".join(seg.text for seg in self.transcript_segments)

    def _capture_audio(self):
        """Capture audio from microphone."""
        import pyaudio

        p = pyaudio.PyAudio()

        stream = p.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=self.sample_rate,
            input=True,
            frames_per_buffer=int(self.sample_rate * 0.1),  # 100ms buffers
        )

        chunk_samples = int(self.sample_rate * self.chunk_duration)
        audio_buffer = []

        while self.is_running:
            try:
                data = stream.read(int(self.sample_rate * 0.1), exception_on_overflow=False)
                audio_buffer.append(data)

                # When we have enough for a chunk
                total_samples = sum(len(d) // 2 for d in audio_buffer)
                if total_samples >= chunk_samples:
                    # Combine and send to queue
                    combined = b"".join(audio_buffer)
                    self.audio_queue.put(combined)

                    # Keep overlap
                    overlap_samples = int(self.sample_rate * self.overlap) * 2
                    audio_buffer = [combined[-overlap_samples:]]

            except Exception as e:
                logger.error(f"Audio capture error: {e}")
                break

        stream.stop_stream()
        stream.close()
        p.terminate()

    def _transcribe_loop(self):
        """Process audio chunks and transcribe."""
        # Load model once
        try:
            faster_model = _get_faster_whisper_model(self.model_name)
            use_faster = faster_model is not None
            if not use_faster:
                model = _get_whisper_model(self.model_name)
        except Exception as e:
            logger.error(f"Failed to load model: {e}")
            return

        while self.is_running:
            try:
                # Get audio chunk (with timeout so we can check is_running)
                try:
                    audio_data = self.audio_queue.get(timeout=0.5)
                except queue.Empty:
                    continue

                # Save to temp file
                with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
                    temp_path = f.name
                    self._write_wav(f, audio_data)

                try:
                    # Transcribe
                    if use_faster:
                        segments, info = faster_model.transcribe(
                            temp_path,
                            language=self.language if self.language != "auto" else None,
                            vad_filter=True,
                        )
                        text_parts = [seg.text.strip() for seg in segments]
                        text = " ".join(text_parts)
                    else:
                        result = model.transcribe(
                            temp_path, language=self.language if self.language != "auto" else None
                        )
                        text = result["text"].strip()

                    if text:
                        segment = TranscriptSegment(
                            start=len(self.transcript_segments) * self.chunk_duration,
                            end=(len(self.transcript_segments) + 1) * self.chunk_duration,
                            text=text,
                        )
                        self.transcript_segments.append(segment)

                        if self._callback:
                            self._callback(text)

                finally:
                    os.unlink(temp_path)

            except Exception as e:
                logger.error(f"Transcription error: {e}")

    def _write_wav(self, f, audio_data):
        """Write audio data to WAV file."""
        with wave.open(f, "wb") as wav:
            wav.setnchannels(1)
            wav.setsampwidth(2)
            wav.setframerate(self.sample_rate)
            wav.writeframes(audio_data)


# ============================================================
# SPEAKER DIARIZATION
# ============================================================


def _diarize_audio(audio_path: str, num_speakers: int = None) -> list:
    """
    Identify different speakers in audio (who spoke when).

    Requires: pip install pyannote.audio (needs HuggingFace token)

    Returns list of (start, end, speaker_id) tuples.
    """
    try:
        from pyannote.audio import Pipeline
    except ImportError:
        logger.debug("pyannote.audio not available, skipping diarization")
        return []

    # Check for HuggingFace token
    hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
    if not hf_token:
        logger.warning("No HuggingFace token found, skipping diarization")
        return []

    try:
        pipeline = Pipeline.from_pretrained(
            "pyannote/speaker-diarization-3.1", use_auth_token=hf_token
        )

        diarization = pipeline(audio_path, num_speakers=num_speakers)

        segments = []
        for turn, _, speaker in diarization.itertracks(yield_label=True):
            segments.append({"start": turn.start, "end": turn.end, "speaker": speaker})

        return segments

    except Exception as e:
        logger.error(f"Diarization failed: {e}")
        return []


def _merge_transcript_with_diarization(transcript: Transcript, diarization: list) -> Transcript:
    """Merge speaker labels into transcript segments."""
    if not diarization:
        return transcript

    # For each transcript segment, find the overlapping speaker
    for seg in transcript.segments:
        seg_start = seg["start"]
        seg_end = seg["end"]

        # Find speaker with most overlap
        best_speaker = None
        best_overlap = 0

        for d in diarization:
            # Calculate overlap
            overlap_start = max(seg_start, d["start"])
            overlap_end = min(seg_end, d["end"])
            overlap = max(0, overlap_end - overlap_start)

            if overlap > best_overlap:
                best_overlap = overlap
                best_speaker = d["speaker"]

        seg["speaker"] = best_speaker

    return transcript


# ============================================================
# TEXT-TO-SPEECH
# ============================================================


def _get_tts_engine():
    """Get or initialize TTS engine."""
    global _tts_engine

    config = _load_voice_config()

    # Try pyttsx3 first (offline)
    try:
        import pyttsx3

        if _tts_engine is None:
            _tts_engine = pyttsx3.init()
            _tts_engine.setProperty("rate", config.get("tts_rate", 150))
            _tts_engine.setProperty("volume", config.get("tts_volume", 1.0))

        return _tts_engine, "pyttsx3"
    except (ImportError, RuntimeError, OSError) as e:
        logger.debug(f"pyttsx3 not available: {e}")

    # Fall back to gTTS (online)
    try:
        from gtts import gTTS  # noqa: F401

        return None, "gtts"
    except ImportError as e:
        logger.debug("Optional dependency not available: %s", e)
    # Try edge-tts (Microsoft, free, good quality)
    try:
        import edge_tts  # noqa: F401

        return None, "edge_tts"
    except ImportError as e:
        logger.debug("Optional dependency not available: %s", e)
    return None, None


def _speak_pyttsx3(text: str, save_path: str = None) -> str:
    """Speak using pyttsx3."""
    engine, _ = _get_tts_engine()

    if save_path:
        engine.save_to_file(text, save_path)
        engine.runAndWait()
        return save_path
    else:
        engine.say(text)
        engine.runAndWait()
        return None


def _speak_gtts(text: str, save_path: str = None, lang: str = "en") -> str:
    """Speak using Google TTS."""
    import subprocess

    from gtts import gTTS

    tts = gTTS(text=text, lang=lang)

    if save_path:
        tts.save(save_path)
        return save_path

    # Save to temp and play
    with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
        temp_path = f.name
        tts.save(temp_path)

    # Try to play
    played = False
    for player in ["mpg123", "afplay", "aplay", "ffplay"]:
        try:
            subprocess.run(
                [player, "-q" if player == "mpg123" else "", temp_path],
                timeout=120,
                capture_output=True,
            )
            played = True
            break
        except (subprocess.SubprocessError, FileNotFoundError, OSError) as e:
            logger.debug("Subprocess error suppressed: %s", e)
            continue
    if not played:
        return temp_path  # Return path if couldn't play

    os.unlink(temp_path)
    return None


async def _speak_edge_tts(text: str, save_path: str = None, voice: str = "en-US-AriaNeural") -> str:
    """Speak using Microsoft Edge TTS (high quality, free)."""
    import edge_tts

    if save_path is None:
        fd, save_path = tempfile.mkstemp(suffix=".mp3")
        os.close(fd)

    communicate = edge_tts.Communicate(text, voice)
    await communicate.save(save_path)

    return save_path


# ============================================================
# TOOL HANDLERS
# ============================================================


def transcribe_audio(data: dict) -> str:
    """
    Transcribe an audio file to text using Whisper.

    Supports any audio format (wav, mp3, m4a, flac, ogg, etc.)
    """
    filepath = data.get("file", "")
    model = data.get("model")  # tiny, base, small, medium, large
    language = data.get("language")  # auto, en, es, fr, etc.
    translate = data.get("translate", False)  # Translate to English
    timestamps = data.get("timestamps", False)  # Include timestamps
    save_transcript = data.get("save", False)

    if not filepath:
        return "Please provide an audio file path"

    filepath = Path(filepath).expanduser()

    # Check uploads directory
    if not filepath.exists():
        uploads_path = Path("/mnt/user-data/uploads") / filepath.name
        if uploads_path.exists():
            filepath = uploads_path
        else:
            return f"File not found: {filepath}"

    try:
        task = "translate" if translate else "transcribe"

        transcript = _transcribe_with_whisper(
            str(filepath),
            model_name=model,
            language=language,
            task=task,
            word_timestamps=timestamps,
        )

        # Build output
        lines = []

        if timestamps and transcript.segments:
            for seg in transcript.segments:
                start = _format_timestamp(seg["start"])
                end = _format_timestamp(seg["end"])
                speaker = f"[{seg['speaker']}] " if seg.get("speaker") else ""
                lines.append(f"[{start} → {end}] {speaker}{seg['text']}")
        else:
            lines.append(transcript.text)

        lines.append(f"\n---\nDuration: {_format_timestamp(transcript.duration)}")
        lines.append(f"Language: {transcript.language}")
        lines.append(f"Model: {transcript.model_used}")

        # Save if requested
        if save_transcript:
            transcript_file = (
                TRANSCRIPTS_DIR / f"{filepath.stem}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
            )
            transcript_file.write_text(json.dumps(asdict(transcript), indent=2))
            lines.append(f"Saved: {transcript_file}")

        return "\n".join(lines)

    except Exception as e:
        logger.error(f"Transcription error: {e}")
        return f"❌ Transcription failed: {e}"


def transcribe_meeting(data: dict) -> str:
    """
    Transcribe a meeting or interview with speaker identification.
    """
    filepath = data.get("file", "")
    num_speakers = data.get("speakers")  # Expected number of speakers
    model = data.get("model")
    save = data.get("save", True)

    if not filepath:
        return "Please provide an audio file path"

    filepath = Path(filepath).expanduser()

    if not filepath.exists():
        uploads_path = Path("/mnt/user-data/uploads") / filepath.name
        if uploads_path.exists():
            filepath = uploads_path
        else:
            return f"File not found: {filepath}"

    try:
        # First, transcribe
        transcript = _transcribe_with_whisper(str(filepath), model_name=model)

        # Then, diarize
        diarization = _diarize_audio(str(filepath), num_speakers)

        if diarization:
            transcript = _merge_transcript_with_diarization(transcript, diarization)

        # Format output
        lines = ["📋 **Meeting Transcript**\n"]

        current_speaker = None
        for seg in transcript.segments:
            speaker = seg.get("speaker", "Unknown")

            if speaker != current_speaker:
                lines.append(f"\n**{speaker}** ({_format_timestamp(seg['start'])})")
                current_speaker = speaker

            lines.append(f"  {seg['text']}")

        lines.append("\n---")
        lines.append(f"Duration: {_format_timestamp(transcript.duration)}")

        if diarization:
            unique_speakers = set(d["speaker"] for d in diarization)
            lines.append(f"Speakers identified: {len(unique_speakers)}")

        # Save
        if save:
            transcript_file = (
                TRANSCRIPTS_DIR / f"meeting_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
            )
            transcript_file.write_text(json.dumps(asdict(transcript), indent=2))
            lines.append(f"Saved: {transcript_file}")

        return "\n".join(lines)

    except Exception as e:
        logger.error(f"Meeting transcription error: {e}")
        return f"❌ Error: {e}"


def start_realtime_transcription(data: dict) -> str:
    """Start real-time speech transcription from microphone."""
    global _recording_thread, _is_recording

    if _is_recording:
        return "Already recording. Use stop_transcription first."

    model = data.get("model", "tiny")
    language = data.get("language", "en")

    try:
        transcriber = RealtimeTranscriber(model_name=model, language=language)

        # Store for later
        _recording_thread = transcriber
        _is_recording = True

        transcriber.start(callback=lambda text: logger.info(f"Heard: {text}"))

        return "🎤 Listening... Use stop_transcription to finish."

    except Exception as e:
        _is_recording = False
        return f"❌ Error starting transcription: {e}"


def stop_realtime_transcription(data: dict) -> str:
    """Stop real-time transcription and return results."""
    global _recording_thread, _is_recording

    if not _is_recording or _recording_thread is None:
        return "No active transcription session."

    try:
        transcript = _recording_thread.stop()
        _is_recording = False
        _recording_thread = None

        if transcript:
            return f"📝 Transcription:\n\n{transcript}"
        else:
            return "🎤 (no speech detected)"

    except Exception as e:
        _is_recording = False
        _recording_thread = None
        return f"❌ Error: {e}"


def speak_text(data: dict) -> str:
    """Convert text to speech and play/save it."""
    text = data.get("text", "")
    save_file = data.get("save_file")
    voice = data.get("voice")

    if not text:
        return "No text to speak"

    engine, engine_type = _get_tts_engine()

    save_path = None
    if save_file:
        save_path = str(AUDIO_DIR / f"{save_file}.mp3")

    try:
        if engine_type == "pyttsx3":
            result = _speak_pyttsx3(text, save_path)
            if result:
                return f"🔊 Saved to {result}"
            return "🔊 (spoken)"

        elif engine_type == "gtts":
            result = _speak_gtts(text, save_path)
            if result:
                return f"🔊 Saved to {result}"
            return "🔊 (spoken)"

        elif engine_type == "edge_tts":
            import asyncio

            voice = voice or "en-US-AriaNeural"
            result = asyncio.run(_speak_edge_tts(text, save_path, voice))
            if save_path:
                return f"🔊 Saved to {result}"
            # Play it
            import subprocess

            subprocess.run(["mpg123", "-q", result], timeout=120, capture_output=True)
            os.unlink(result)
            return "🔊 (spoken)"

        else:
            return "❌ No TTS engine available. Install pyttsx3, gtts, or edge-tts."

    except Exception as e:
        return f"❌ TTS error: {e}"


def listen_microphone(data: dict) -> str:
    """Listen to microphone and transcribe (single utterance)."""
    duration = data.get("duration", 5)
    model = data.get("model", "tiny")

    try:
        import wave

        import pyaudio
    except ImportError:
        from familiar.core.deps import PYAUDIO_PACKAGES, ensure_packages

        ok, _ = ensure_packages(PYAUDIO_PACKAGES)
        if ok:
            import pyaudio
        else:
            return "❌ Microphone capture not available — pyaudio requires the PortAudio system library."

    try:
        # Record audio
        p = pyaudio.PyAudio()

        sample_rate = 16000
        chunk = 1024

        stream = p.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=sample_rate,
            input=True,
            frames_per_buffer=chunk,
        )

        logger.info(f"🎤 Listening for {duration} seconds...")

        frames = []
        for _ in range(int(sample_rate / chunk * duration)):
            data_chunk = stream.read(chunk, exception_on_overflow=False)
            frames.append(data_chunk)

        stream.stop_stream()
        stream.close()
        p.terminate()

        # Save to temp file
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
            temp_path = f.name
            with wave.open(f, "wb") as wf:
                wf.setnchannels(1)
                wf.setsampwidth(2)
                wf.setframerate(sample_rate)
                wf.writeframes(b"".join(frames))

        try:
            # Transcribe
            transcript = _transcribe_with_whisper(temp_path, model_name=model)

            if transcript.text.strip():
                return f"🎤 Heard: {transcript.text}"
            else:
                return "🎤 (no speech detected)"

        finally:
            os.unlink(temp_path)

    except Exception as e:
        return f"❌ Error: {e}"


def set_voice_config(data: dict) -> str:
    """Update voice configuration."""
    config = _load_voice_config()

    changed = []

    if "model" in data:
        config["whisper_model"] = data["model"]
        changed.append(f"Whisper model: {data['model']}")
        # Clear cached model
        global _whisper_model, _whisper_model_name
        _whisper_model = None
        _whisper_model_name = None

    if "language" in data:
        config["language"] = data["language"]
        changed.append(f"Language: {data['language']}")

    if "rate" in data:
        config["tts_rate"] = data["rate"]
        changed.append(f"TTS rate: {data['rate']}")

    if "volume" in data:
        config["tts_volume"] = data["volume"]
        changed.append(f"TTS volume: {data['volume']}")

    if changed:
        _save_voice_config(config)
        return "✓ Updated:\n" + "\n".join(f"  • {c}" for c in changed)

    # Show current config
    return (
        "Current voice settings:\n"
        f"  • Whisper model: {config.get('whisper_model', 'tiny')}\n"
        f"  • Language: {config.get('language', 'auto')}\n"
        f"  • TTS rate: {config.get('tts_rate', 150)}\n"
        f"  • TTS volume: {config.get('tts_volume', 1.0)}"
    )


def list_whisper_models(data: dict) -> str:
    """List available Whisper models with descriptions."""
    models = [
        ("tiny", "~75MB", "~1GB RAM", "Fastest, good for Pi 4, basic accuracy"),
        ("base", "~150MB", "~1.5GB RAM", "Better accuracy, still fast"),
        ("small", "~500MB", "~2GB RAM", "Good balance of speed and accuracy"),
        ("medium", "~1.5GB", "~5GB RAM", "Very accurate, needs good CPU"),
        ("large", "~3GB", "~10GB RAM", "Best accuracy, needs GPU recommended"),
        ("large-v3", "~3GB", "~10GB RAM", "Latest, best multilingual support"),
    ]

    lines = ["Available Whisper models:\n"]

    for name, disk, ram, desc in models:
        lines.append(f"  **{name}**")
        lines.append(f"    Disk: {disk} | RAM: {ram}")
        lines.append(f"    {desc}\n")

    config = _load_voice_config()
    lines.append(f"Currently using: {config.get('whisper_model', 'tiny')}")

    return "\n".join(lines)


# ============================================================
# UTILITY FUNCTIONS
# ============================================================


def _format_timestamp(seconds: float) -> str:
    """Format seconds as HH:MM:SS or MM:SS."""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = int(seconds % 60)

    if hours > 0:
        return f"{hours:02d}:{minutes:02d}:{secs:02d}"
    return f"{minutes:02d}:{secs:02d}"


# ============================================================
# TOOL DEFINITIONS
# ============================================================

TOOLS = [
    {
        "name": "transcribe",
        "description": (
            "Transcribe an audio file to text using Whisper AI with advanced options including language detection, translation to English, and word-level timestamps. "
            "Supports any format that ffmpeg can decode (wav, mp3, m4a, flac, ogg, etc.). "
            "Preferred over transcribe_audio when you need timestamps, translation, or model selection. "
            "Optionally saves the full transcript with segments to a JSON file."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "file": {"type": "string", "description": "Path to audio file"},
                "model": {
                    "type": "string",
                    "description": "Whisper model size: tiny, base, small, medium, large",
                    "enum": ["tiny", "base", "small", "medium", "large", "large-v3"],
                },
                "language": {
                    "type": "string",
                    "description": "Language code (en, es, fr, etc.) or 'auto' for detection",
                },
                "translate": {
                    "type": "boolean",
                    "description": "Translate to English (for non-English audio)",
                    "default": False,
                },
                "timestamps": {
                    "type": "boolean",
                    "description": "Include timestamps for each segment",
                    "default": False,
                },
                "save": {
                    "type": "boolean",
                    "description": "Save transcript to file",
                    "default": False,
                },
            },
            "required": ["file"],
        },
        "handler": transcribe_audio,
        "category": "voice",
    },
    {
        "name": "transcribe_meeting",
        "description": (
            "Transcribe a meeting or interview with speaker identification via pyannote diarization. "
            "Combines Whisper transcription with speaker labels to show who said what and when. "
            "Requires a HuggingFace token (HF_TOKEN) for the diarization model. "
            "Saves the full transcript with speaker segments to a JSON file by default."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "file": {"type": "string", "description": "Path to audio file"},
                "speakers": {
                    "type": "integer",
                    "description": "Expected number of speakers (helps accuracy)",
                },
                "model": {
                    "type": "string",
                    "description": "Whisper model size",
                    "enum": ["tiny", "base", "small", "medium", "large"],
                },
                "save": {
                    "type": "boolean",
                    "default": True,
                    "description": "Save transcript to file",
                },
            },
            "required": ["file"],
        },
        "handler": transcribe_meeting,
        "category": "voice",
    },
    {
        "name": "listen",
        "description": (
            "Listen to the microphone for a specified duration and transcribe the captured speech as a single utterance. "
            "Requires pyaudio and the PortAudio system library. "
            "Use this for quick voice input; for continuous transcription, use start_live_transcription instead. "
            "Returns the transcribed text or indicates if no speech was detected."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "duration": {
                    "type": "integer",
                    "description": "How many seconds to listen",
                    "default": 5,
                },
                "model": {
                    "type": "string",
                    "description": "Whisper model to use",
                    "default": "tiny",
                },
            },
        },
        "handler": listen_microphone,
        "category": "voice",
    },
    {
        "name": "start_live_transcription",
        "description": (
            "Start continuous real-time transcription from the microphone using streaming audio chunks. "
            "Runs in the background until stopped with stop_live_transcription. "
            "Requires pyaudio and PortAudio; the tiny Whisper model is recommended for real-time performance. "
            "Each chunk is transcribed independently and appended to a running transcript."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "model": {
                    "type": "string",
                    "description": "Whisper model (tiny recommended for real-time)",
                    "default": "tiny",
                },
                "language": {
                    "type": "string",
                    "default": "en",
                    "description": "Language code for transcription",
                },
            },
        },
        "handler": start_realtime_transcription,
        "category": "voice",
    },
    {
        "name": "stop_live_transcription",
        "description": (
            "Stop an active real-time transcription session and return the full accumulated transcript. "
            "Must be called after start_live_transcription to retrieve results. "
            "Returns the concatenated text from all transcribed audio chunks."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": stop_realtime_transcription,
        "category": "voice",
    },
    {
        "name": "speak",
        "description": (
            "Convert text to speech and play it through the system audio output. "
            "Tries pyttsx3 (offline), gTTS (Google, online), or edge-tts (Microsoft, free) in order of availability. "
            "Optionally saves the generated audio to a file instead of playing it. "
            "Use voice_settings to adjust speech rate and volume."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "text": {"type": "string", "description": "Text to speak"},
                "save_file": {"type": "string", "description": "Optional filename to save audio"},
                "voice": {"type": "string", "description": "Voice to use (for edge-tts)"},
            },
            "required": ["text"],
        },
        "handler": speak_text,
        "category": "voice",
    },
    {
        "name": "voice_settings",
        "description": (
            "View or update voice and transcription settings including default Whisper model, language, TTS speech rate, and volume. "
            "Call with no parameters to view current settings. "
            "Changing the Whisper model clears the cached model so the new one loads on next use. "
            "Settings are persisted to voice_config.json."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "model": {"type": "string", "description": "Default Whisper model"},
                "language": {"type": "string", "description": "Default language"},
                "rate": {"type": "integer", "description": "TTS speech rate"},
                "volume": {"type": "number", "description": "TTS volume (0.0 to 1.0)"},
            },
        },
        "handler": set_voice_config,
        "category": "voice",
    },
    {
        "name": "list_models",
        "description": (
            "List available Whisper transcription models with disk size, RAM requirements, and accuracy descriptions. "
            "Models range from tiny (~75MB, good for Raspberry Pi) to large-v3 (~3GB, best multilingual accuracy). "
            "Shows which model is currently configured as the default."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": list_whisper_models,
        "category": "voice",
    },
    # Document Reader Tools (Piper TTS)
    {
        "name": "voice_read_document",
        "description": (
            "Read a document aloud using local HIPAA-compliant Piper TTS, supporting DOCX, PDF, TXT, MD, and HTML formats. "
            "Can read the entire document or a specific section by number. "
            "All processing happens locally with no data sent to external services. "
            "Optionally saves the generated audio to a file."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "file": {"type": "string", "description": "Path to document file"},
                "section": {
                    "type": "integer",
                    "description": "Section number to read (optional, reads all if not specified)",
                },
                "voice": {
                    "type": "string",
                    "description": "Voice ID (default: en_US-lessac-medium)",
                    "default": "en_US-lessac-medium",
                },
                "speed": {
                    "type": "number",
                    "description": "Speaking speed 0.5-2.0 (default: 1.0)",
                    "default": 1.0,
                },
                "save": {"type": "string", "description": "Filename to save audio (optional)"},
                "list_sections": {
                    "type": "boolean",
                    "description": "If true, list sections without reading",
                    "default": False,
                },
            },
            "required": ["file"],
        },
        "handler": read_document if DOCUMENT_READER_AVAILABLE else None,
        "category": "voice",
    },
    {
        "name": "stop_reading",
        "description": (
            "Stop the currently active document reading session. "
            "Use this to interrupt a long document that is being read aloud via voice_read_document. "
            "Has no effect if nothing is currently being read."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": stop_reading if DOCUMENT_READER_AVAILABLE else None,
        "category": "voice",
    },
    {
        "name": "reading_status",
        "description": (
            "Get the current status of any active document reading session. "
            "Shows whether a document is being read, which section is playing, and progress information. "
            "Returns idle status if no document is currently being read."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": get_reading_status if DOCUMENT_READER_AVAILABLE else None,
        "category": "voice",
    },
    {
        "name": "list_tts_voices",
        "description": (
            "List available Piper TTS voices for document reading, showing voice IDs and language information. "
            "Piper voices run entirely locally for HIPAA-compliant text-to-speech. "
            "Use the voice ID with voice_read_document to select a specific voice."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": list_voices if DOCUMENT_READER_AVAILABLE else None,
        "category": "voice",
    },
    {
        "name": "install_piper",
        "description": (
            "Get installation instructions for Piper TTS, a local HIPAA-compliant text-to-speech engine. "
            "Piper runs entirely offline with no data sent to external services. "
            "Provides platform-specific setup steps and voice download instructions."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": install_piper if DOCUMENT_READER_AVAILABLE else None,
        "category": "voice",
    },
]
