#!/usr/bin/env python3
"""
Pipeline completo: micrófono → ffmpeg → Whisper → Ollama → TTS → ffmpeg → altavoz
================================================================================

Este script une todo lo visto en el arco de audio/vídeo de la Temporada 9:
  842 — ffmpeg (preparación y postprocesado de audio)
  843 — Whisper (transcripción local)
  845 — TTS con Piper/Kokoro (voz sintética)
  847 — visión artificial local (LLaVA, Florence-2, Moondream)

Requisitos:
  pip install faster-whisper requests piper-tts

  Además necesitas tener instalado:
    - ffmpeg (sudo apt install ffmpeg)
    - Ollama corriendo en localhost:11434
    - Piper TTS con voz en español (es_ES-sharvard-medium.onnx)

atareao con Linux — Episodio 842
"""

import subprocess
import tempfile
import time
from pathlib import Path
from typing import Optional

import requests

# ── Configuración ────────────────────────────────────────────────────────
OLLAMA_URL = "http://localhost:11434/api/generate"
OLLAMA_MODEL = "llama3.2:latest"


# ── Colores para terminal ────────────────────────────────────────────────
class Color:
    CYAN = "\033[0;36m"
    GREEN = "\033[0;32m"
    YELLOW = "\033[1;33m"
    RED = "\033[0;31m"
    NC = "\033[0m"


def print_step(emoji: str, msg: str) -> None:
    """Imprime un paso del pipeline con formato."""
    print(f"\n{emoji} {Color.CYAN}{msg}{Color.NC}")
    print("─" * 50)


def print_ok(msg: str) -> None:
    print(f"   {Color.GREEN}✅{Color.NC} {msg}")


def print_warn(msg: str) -> None:
    print(f"   {Color.YELLOW}⚠️{Color.NC} {msg}")


# ── Paso 1: Grabar y preparar con ffmpeg ────────────────────────────────
def grabar_y_preparar(segundos: int = 5) -> Optional[str]:
    """
    Graba audio del micrófono y lo prepara para Whisper con ffmpeg.

    1. arecord captura el audio en bruto
    2. ffmpeg lo convierte a: 16 kHz, mono, s16le + loudnorm

    Returns:
        Ruta al archivo WAV listo para Whisper, o None si falla.
    """
    print_step("🎤", f"Grabando {segundos} segundos...")

    # Archivo temporal para la grabación en bruto
    with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
        raw_path = f.name

    # Archivo para el audio preparado
    whisper_path = raw_path.replace(".wav", "_whisper.wav")

    # ── arecord: grabar desde el micrófono ──────────────────────────
    # PipeWire expone el micrófono como dispositivo PulseAudio.
    # arecord es la herramienta CLI de ALSA/PulseAudio para grabar.
    try:
        subprocess.run(
            ["arecord", "-f", "cd", "-t", "wav", "-d", str(segundos), raw_path],
            capture_output=True,
            check=True,
        )
        print_ok(f"Grabación cruda: {raw_path}")
    except subprocess.CalledProcessError as e:
        print_warn(f"Error al grabar: {e.stderr.decode().strip()}")
        print_warn("Asegúrate de tener el micrófono conectado y PipeWire funcionando.")
        Path(raw_path).unlink(missing_ok=True)
        return None
    except FileNotFoundError:
        print_warn("arecord no encontrado. Instala alsa-utils o pulseaudio-utils.")
        return None

    # ── ffmpeg: preparar para Whisper ───────────────────────────────
    # Esto es el comando estrella del Episodio 842:
    #   -ar 16000      → 16 kHz (Whisper lo espera así)
    #   -ac 1          → Mono
    #   -c:a pcm_s16le → Formato nativo WAV
    #   -af loudnorm   → Normalización EBU R128
    print_step("⚡", "Preparando audio con ffmpeg (16 kHz, mono, loudnorm)...")

    result = subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-i",
            raw_path,
            "-ar",
            "16000",
            "-ac",
            "1",
            "-c:a",
            "pcm_s16le",
            "-af",
            "loudnorm",
            whisper_path,
        ],
        capture_output=True,
        check=False,
    )

    # Limpiar grabación cruda
    Path(raw_path).unlink(missing_ok=True)

    if result.returncode != 0 or not Path(whisper_path).exists():
        print_warn(f"Error en ffmpeg: {result.stderr.decode()[:200]}")
        return None

    # Mostrar info del archivo resultante
    size = Path(whisper_path).stat().st_size
    print_ok(f"Audio preparado: {whisper_path} ({size // 1024} KB)")
    print_ok("Formato: 16 kHz / Mono / PCM s16le / Normalizado")
    return whisper_path


# ── Paso 2: Transcribir con Whisper ─────────────────────────────────────
def transcribir(audio_path: str) -> str:
    """
    Transcribe el audio preparado con faster-whisper.

    faster-whisper es una reimplementación de Whisper usando CTranslate2,
    que es 4x más rápida que la original y usa menos memoria.

    Args:
        audio_path: Ruta al WAV preparado con ffmpeg.

    Returns:
        Texto transcrito, o cadena vacía si falla.
    """
    print_step("📝", "Transcribiendo con Whisper...")

    try:
        from faster_whisper import WhisperModel
    except ImportError:
        print_warn("faster-whisper no instalado. Ejecuta:")
        print_warn("  pip install faster-whisper")
        return ""

    # Cargar modelo (descarga automática la primera vez)
    print_ok("Cargando modelo Whisper 'base'...")
    t0 = time.time()
    model = WhisperModel("base", device="cpu", compute_type="int8")
    print_ok(f"Modelo cargado en {time.time() - t0:.1f}s")

    # Transcribir
    # language="es" fuerza español (evita que detecte mezcla)
    # beam_size=1 es más rápido, beam_size>1 más preciso
    segments, info = model.transcribe(
        audio_path,
        language="es",
        beam_size=1,
    )

    texto = " ".join(segment.text for segment in segments)
    print_ok(
        f"Idioma detectado: {info.language} (prob: {info.language_probability:.2f})"
    )

    # Limpiar archivo temporal
    Path(audio_path).unlink(missing_ok=True)

    return texto.strip()


# ── Paso 3: Consultar a Ollama ──────────────────────────────────────────
def consultar_ollama(prompt: str) -> str:
    """
    Envía el texto transcrito a Ollama y obtiene una respuesta.

    Ollama corre el modelo localmente (llama3.2, mistral, etc.).
    Nada se envía a la nube.

    Args:
        prompt: Texto transcrito del usuario.

    Returns:
        Respuesta del modelo, o mensaje de error.
    """
    print_step("🧠", "Consultando Ollama...")

    payload = {
        "model": OLLAMA_MODEL,
        "prompt": prompt,
        "stream": False,
    }

    try:
        t0 = time.time()
        resp = requests.post(OLLAMA_URL, json=payload, timeout=60)
        resp.raise_for_status()
        respuesta = resp.json().get("response", "")
        print_ok(f"Respuesta generada en {time.time() - t0:.1f}s")
        return respuesta
    except requests.exceptions.ConnectionError:
        print_warn(f"No se pudo conectar a Ollama en {OLLAMA_URL}")
        print_warn("¿Está Ollama corriendo?")
        return "Lo siento, no pude conectar con el asistente."
    except requests.exceptions.Timeout:
        print_warn("Ollama tardó demasiado en responder.")
        return "Lo siento, la respuesta tardó demasiado."
    except Exception as e:
        print_warn(f"Error inesperado: {e}")
        return ""


# ── Paso 4: Responder con TTS + ffmpeg ─────────────────────────────────
def responder(texto: str) -> None:
    """
    Genera voz sintética con Piper y la postprocesa con ffmpeg.

    El pipeline de audio es:
      1. Piper genera WAV crudo
      2. ffmpeg postprocesa con: loudnorm + compand + highpass
      3. aplay reproduce el resultado

    Args:
        texto: Texto a convertir a voz.
    """
    if not texto:
        return

    print_step("🔊", "Generando voz con TTS + ffmpeg...")

    try:
        import wave
        from piper import PiperVoice
    except ImportError:
        print_warn("piper-tts no instalado. Ejecuta:")
        print_warn("  pip install piper-tts")
        print_warn(
            "  (o descarga el modelo .onnx desde https://github.com/rhasspy/piper)"
        )
        # Fallback: mostrar el texto por pantalla
        print(f"\n{Color.GREEN}🤖 IA:{Color.NC} {texto}")
        return

    # ── Generar WAV con Piper ──────────────────────────────────────
    tts_path = "/tmp/tts_raw.wav"

    try:
        # Cargar modelo de voz en español
        # Descarga el modelo desde:
        # https://huggingface.co/rhasspy/piper-voices/tree/main/es/es_ES/sharvard/medium
        voice = PiperVoice.load("es_ES-sharvard-medium.onnx")

        with wave.open(tts_path, "wb") as wav_file:
            voice.synthesize_wav(texto, wav_file)

        print_ok(f"TTS generado: {tts_path}")
    except Exception as e:
        print_warn(f"Error al generar TTS con Piper: {e}")
        print_warn("Asegúrate de tener el modelo 'es_ES-sharvard-medium.onnx'")
        # Fallback
        print(f"\n{Color.GREEN}🤖 IA:{Color.NC} {texto[:200]}...")
        return

    # ── Postprocesar con ffmpeg ────────────────────────────────────
    # Este es el SEGUNDO comando estrella del Episodio 842:
    #   loudnorm   → Normaliza el volumen
    #   compand    → Comprime el rango dinámico (equilibra suaves y fuertes)
    #   highpass   → Elimina frecuencias < 80 Hz (soplidos)
    final_path = "/tmp/tts_final.wav"

    filter_chain = (
        "loudnorm,"
        "compand=attacks=0.1:decays=0.2:"
        "points=-80/-80|-45/-45|-30/-30|-25/-20|-20/-15"
        "|-15/-12|-10/-6|-6/-3|0/-0.5,"
        "highpass=f=80"
    )

    result = subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-i",
            tts_path,
            "-af",
            filter_chain,
            final_path,
        ],
        capture_output=True,
        check=False,
    )

    if result.returncode != 0 or not Path(final_path).exists():
        print_warn(f"Error en ffmpeg post-procesado: {result.stderr.decode()[:200]}")
        # Reproducir el TTS crudo como fallback
        subprocess.run(["aplay", "-q", tts_path], capture_output=True)
        Path(tts_path).unlink(missing_ok=True)
        return

    size = Path(final_path).stat().st_size
    print_ok(f"Audio postprocesado con ffmpeg: {final_path} ({size // 1024} KB)")
    print_ok("Filtros aplicados: loudnorm + compand + highpass")

    # ── Reproducir con aplay ───────────────────────────────────────
    print_step("▶️", "Reproduciendo respuesta...")
    subprocess.run(["aplay", "-q", final_path], capture_output=True)
    print_ok("Reproducción completada")

    # ── Limpiar archivos temporales ────────────────────────────────
    Path(tts_path).unlink(missing_ok=True)
    Path(final_path).unlink(missing_ok=True)
    print_ok("Archivos temporales eliminados")


# ── Bucle principal ─────────────────────────────────────────────────────
def main() -> None:
    """
    Bucle principal del asistente de voz.

    Pipeline completo:
      Micrófono → [arecord] → ffmpeg → [Whisper] → Ollama → [Piper] → ffmpeg → [aplay]
    """
    print("\n" + "═" * 60)
    print(f"{Color.CYAN}🎤 Asistente de voz local{Color.NC}")
    print("Episodio 842 — atareao con Linux")
    print(
        f"{Color.GREEN}Mic → ffmpeg → Whisper → Ollama → TTS → ffmpeg → Altavoz{Color.NC}"
    )
    print("═" * 60)
    print(f"\n{Color.YELLOW}Habla. Pulsa Ctrl+C para salir.{Color.NC}\n")

    # Verificar dependencias al inicio
    dependencies_ok = True

    # Verificar ffmpeg
    if not subprocess.run(["which", "ffmpeg"], capture_output=True).returncode == 0:
        print_warn("ffmpeg no está instalado. Ejecuta: sudo apt install ffmpeg")
        dependencies_ok = False

    # Verificar Ollama
    try:
        requests.get(OLLAMA_URL.replace("/api/generate", "/api/tags"), timeout=5)
        print_ok(f"Ollama conectado en {OLLAMA_URL}")
    except Exception:
        print_warn(f"Ollama no responde en {OLLAMA_URL}")
        print_warn("¿Está corriendo? Ejecuta: ollama serve && ollama run llama3.2")
        dependencies_ok = False

    # Verificar arecord
    if not subprocess.run(["which", "arecord"], capture_output=True).returncode == 0:
        print_warn("arecord no encontrado. Instala alsa-utils.")
        dependencies_ok = False

    if not dependencies_ok:
        print(
            f"\n{Color.RED}⚠️  Hay dependencias faltantes. Revisa los mensajes anteriores.{Color.NC}"
        )
        print("El script intentará continuar, pero puede fallar en algunos pasos.\n")

    # ── Bucle principal ────────────────────────────────────────────
    try:
        while True:
            print("\n" + "━" * 60)

            # 1. Grabar y preparar con ffmpeg
            audio = grabar_y_preparar(segundos=5)
            if audio is None:
                print_warn("No se pudo grabar. Reintentando...")
                continue

            # 2. Transcribir con Whisper
            texto = transcribir(audio)
            if not texto:
                print_warn("No se detectó voz. ¿Demasiado silencio?")
                continue

            print(f"\n{Color.GREEN}📝 Tú:{Color.NC} {texto}")

            # 3. Consultar a Ollama
            respuesta = consultar_ollama(texto)
            if not respuesta:
                print_warn("No se obtuvo respuesta de Ollama.")
                continue

            # Mostrar preview de la respuesta
            preview = respuesta[:150] + ("..." if len(respuesta) > 150 else "")
            print(f"{Color.YELLOW}🤖 IA: {preview}{Color.NC}")

            # 4. Responder con TTS + ffmpeg
            responder(respuesta)

            print("─" * 60)

    except KeyboardInterrupt:
        print(f"\n\n{Color.GREEN}👋 ¡Hasta luego!{Color.NC}")
        print("Recuerda: ffmpeg es la navaja suiza del audio para tu IA.")
    except Exception as e:
        print(f"\n{Color.RED}❌ Error inesperado:{Color.NC} {e}")
        raise


if __name__ == "__main__":
    main()