"""Discord voice slash commands (Pas 7 — CONVERGENCE wiring).

Registers the `/voice` slash command group on the existing CommandTree and
exposes an async `warmup_models()` for eager model load at bot startup.

Owns nothing in `src/voice/*` — purely the Discord-facing wiring. Defers
heavy lifting to:

- ``src.voice.pipeline.VoiceSession`` — per-guild session state machine
- ``src.voice.pipeline.EchoVoiceSink`` — discord-ext-voice-recv sink
- ``src.voice.tts_stream.TTSQueue`` / ``EchoStreamingAudioSource``
- ``src.voice._discord_voice_adapter.connect_voice``
"""

from __future__ import annotations

import asyncio
import logging
from typing import Optional

import discord
from discord import app_commands

# Optional DAVE dep (mandatory at runtime when discord.py 2.7.1 is paired with
# Discord voice gateway v=8; tolerated missing in tests / dev environments).
try:
    import davey
    _HAS_DAVE = True
except ImportError:
    _HAS_DAVE = False

from src.config import Config
from src.voice.pipeline import (
    VoiceSession,
    EchoVoiceSink,
    _get_whisper_model,
    _get_silero_vad,
)
from src.voice.tts_stream import TTSQueue, EchoStreamingAudioSource
from src.voice._discord_voice_adapter import connect_voice

log = logging.getLogger("echo-core.discord.voice")

# Per-guild voice session registry. Key = guild_id.
_voice_sessions: dict[int, VoiceSession] = {}

# Set if model warmup failed; surfaces as ephemeral error on /voice join.
_voice_load_error: Optional[str] = None

# Reference to the eager warmup task created in on_ready, so /voice join can
# await it if the user is faster than the background load.
_models_warmup_future: Optional[asyncio.Task] = None


async def warmup_models() -> None:
    """Eager model load — called from `on_ready()` as a background task.

    Runs the (synchronous, blocking) model loaders on a worker thread so the
    event loop stays responsive. On failure, sets `_voice_load_error` instead
    of raising, so `/voice join` can degrade gracefully.
    """
    global _voice_load_error
    try:
        if not discord.opus.is_loaded():
            discord.opus.load_opus("libopus.so.0")
        if _HAS_DAVE:
            log.info("DAVE protocol v%d available (davey %s)",
                     davey.DAVE_PROTOCOL_VERSION, davey.__version__)
        await asyncio.to_thread(_get_whisper_model)
        await asyncio.to_thread(_get_silero_vad)
        log.info("Voice models warm")
    except Exception as e:
        _voice_load_error = f"{type(e).__name__}: {e}"
        log.error("Voice models load failed: %s", _voice_load_error)


def _get_whitelist() -> set[int]:
    """Read `voice.allowed_user_ids` from config and coerce to int set.

    Re-reads config from disk to pick up any runtime edits between bot start
    and /voice join.
    """
    try:
        raw = Config().get("voice.allowed_user_ids", [])
    except Exception:
        raw = []
    out: set[int] = set()
    for v in raw or []:
        try:
            out.add(int(v))
        except (TypeError, ValueError):
            continue
    return out


def _get_default_voice() -> str:
    try:
        return Config().get("voice.default_voice", "M2") or "M2"
    except Exception:
        return "M2"


def _default_voice_for_engine(engine: str) -> str:
    """Vocea implicită asociată engine-ului (nu vocea implicită globală per-guild)."""
    if engine == "pockettts":
        return "alba"
    return _get_default_voice()


def register(tree: app_commands.CommandTree, bot: discord.Client) -> app_commands.Group:
    """Build the `/voice` slash command group and return it (caller registers)."""
    voice_group = app_commands.Group(
        name="voice", description="Echo Core voice channel"
    )

    @voice_group.command(name="join", description="Echo intră în voice channel-ul tău")
    async def join(interaction: discord.Interaction) -> None:
        await interaction.response.defer(ephemeral=True)
        if _voice_load_error:
            await interaction.followup.send(
                f"Voice unavailable: {_voice_load_error}", ephemeral=True
            )
            return
        if _models_warmup_future is not None and not _models_warmup_future.done():
            try:
                await _models_warmup_future
            except Exception as e:
                await interaction.followup.send(
                    f"Voice unavailable: {type(e).__name__}: {e}", ephemeral=True
                )
                return
        user = interaction.user
        if not isinstance(user, discord.Member) or user.voice is None or user.voice.channel is None:
            await interaction.followup.send(
                "Intră într-un voice channel întâi.", ephemeral=True
            )
            return
        channel = user.voice.channel
        whitelist = _get_whitelist()
        if user.id not in whitelist:
            await interaction.followup.send(
                "Nu ești pe whitelist voice.", ephemeral=True
            )
            return
        # Reject double-join on the same guild.
        guild_id = channel.guild.id
        if guild_id in _voice_sessions:
            await interaction.followup.send(
                "Sunt deja în voice pe acest server. Folosește /voice leave întâi.",
                ephemeral=True,
            )
            return
        # Connect
        try:
            vc = await connect_voice(channel)
        except Exception as e:
            log.exception("connect_voice failed")
            await interaction.followup.send(
                f"Conectare eșuată: {type(e).__name__}: {e}", ephemeral=True
            )
            return
        # Build TTS queue + session
        ttsq = TTSQueue(voice_id=_get_default_voice(), lang="ro")
        ttsq.start()
        try:
            session = VoiceSession(
                text_channel_id=int(interaction.channel.id),
                voice_channel_id=int(channel.id),
                guild_id=guild_id,
                voice_client=vc,
                record_enabled=False,
                mirror_enabled=True,
                whitelist=whitelist,
                ttsq=ttsq,
                bot=bot,
                loop=asyncio.get_running_loop(),
            )
        except Exception as e:
            log.exception("VoiceSession construction failed")
            ttsq.stop()
            try:
                await vc.disconnect(force=True)
            except Exception:
                pass
            await interaction.followup.send(
                f"Sesiune voice eșuată: {type(e).__name__}: {e}", ephemeral=True
            )
            return
        _voice_sessions[guild_id] = session
        # Start TTS streaming source for the entire session. Chain the
        # wake-up beep via `after=` so streaming takes over when beep ends.
        def _start_stream(error: Optional[Exception] = None) -> None:
            if error is not None:
                log.warning("Beep playback ended with error: %s", error)
            try:
                vc.play(EchoStreamingAudioSource(ttsq))
                log.info("TTS streaming source attached")
            except Exception:
                log.exception("EchoStreamingAudioSource attach failed")
        try:
            vc.play(
                discord.FFmpegPCMAudio("assets/voice/beep_200ms.wav"),
                after=_start_stream,
            )
        except Exception:
            log.warning("Beep playback skipped, starting stream directly", exc_info=True)
            _start_stream()
        # Attach sink
        try:
            bot_user_id = int(bot.user.id) if bot.user is not None else 0
            sink = EchoVoiceSink(session=session, bot_user_id=bot_user_id)
            vc.listen(sink)
        except Exception as e:
            log.exception("Sink attach failed")
            _voice_sessions.pop(guild_id, None)
            try:
                session.cleanup("sink_attach_failed")
            except Exception:
                pass
            await interaction.followup.send(
                f"Atașare sink eșuată: {type(e).__name__}: {e}", ephemeral=True
            )
            return
        # Presence
        try:
            await bot.change_presence(activity=discord.Activity(
                type=discord.ActivityType.listening,
                name=f"{user.display_name} în #{channel.name}",
            ))
        except Exception:
            log.warning("Presence update skipped", exc_info=True)
        await interaction.followup.send(
            f"În voce în #{channel.name}.", ephemeral=True
        )

    @voice_group.command(name="leave", description="Echo iese din voice channel")
    async def leave(interaction: discord.Interaction) -> None:
        await interaction.response.defer(ephemeral=True)
        guild_id = interaction.guild.id if interaction.guild else None
        session = _voice_sessions.pop(guild_id, None) if guild_id is not None else None
        if session is None:
            await interaction.followup.send(
                "Nu sunt în niciun voice channel aici.", ephemeral=True
            )
            return
        try:
            session.cleanup("user_leave")
        except Exception:
            log.exception("session.cleanup raised")
        try:
            await bot.change_presence(activity=None)
        except Exception:
            log.warning("Presence reset skipped", exc_info=True)
        await interaction.followup.send("Plecat.", ephemeral=True)

    _VOICE_CHOICES = [
        app_commands.Choice(name=v, value=v)
        for v in ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5")
    ]

    @voice_group.command(name="setvoice", description="Schimbă vocea Echo (M1-M5 sau F1-F5)")
    @app_commands.describe(voice="Voce nouă")
    @app_commands.choices(voice=_VOICE_CHOICES)
    async def setvoice(
        interaction: discord.Interaction,
        voice: app_commands.Choice[str],
    ) -> None:
        await interaction.response.defer(ephemeral=True)
        new_voice = voice.value
        # Live-swap on the active session if Echo is in voice on this guild.
        guild_id = interaction.guild.id if interaction.guild else None
        session = _voice_sessions.get(guild_id) if guild_id is not None else None
        live_swapped = False
        if session is not None and session.ttsq is not None:
            session.ttsq.voice_id = new_voice
            live_swapped = True
        # Persist as the new default for future sessions.
        try:
            cfg = Config()
            cfg.set("voice.default_voice", new_voice)
            cfg.save()
        except Exception as e:
            log.warning("config save failed for new default voice: %s", e)
            await interaction.followup.send(
                f"Voce schimbată live ({new_voice}), dar config-ul nu s-a salvat: {e}",
                ephemeral=True,
            )
            return
        if live_swapped:
            msg = f"Vocea schimbată **live** pe {new_voice}. Următoarea frază va folosi vocea nouă."
        else:
            msg = f"Default voce setată {new_voice}. Va intra în vigoare la următorul /voice join."
        await interaction.followup.send(msg, ephemeral=True)

    _ENGINE_CHOICES = [
        app_commands.Choice(name="pocket-tts", value="pockettts"),
        app_commands.Choice(name="Supertonic", value="supertonic"),
    ]

    @voice_group.command(name="engine", description="Schimbă engine-ul TTS implicit (pockettts/supertonic)")
    @app_commands.describe(engine="Engine TTS")
    @app_commands.choices(engine=_ENGINE_CHOICES)
    async def engine_cmd(
        interaction: discord.Interaction,
        engine: app_commands.Choice[str],
    ) -> None:
        await interaction.response.defer(ephemeral=True)
        new_engine = engine.value
        try:
            cfg = Config()
            cfg.set("tts.default_engine", new_engine)
            cfg.save()
        except Exception as e:
            log.warning("config save failed for tts.default_engine: %s", e)
            await interaction.followup.send(
                f"Eroare la salvarea engine-ului: {e}", ephemeral=True
            )
            return
        default_voice = _default_voice_for_engine(new_engine)
        await interaction.followup.send(
            f"Engine TTS implicit setat pe **{new_engine}**. Voce implicită: {default_voice}.",
            ephemeral=True,
        )

    @voice_group.command(name="stop", description="Oprește audio-ul curent (golește coada TTS)")
    async def stop_audio(interaction: discord.Interaction) -> None:
        await interaction.response.defer(ephemeral=True)
        guild_id = interaction.guild.id if interaction.guild else None
        session = _voice_sessions.get(guild_id) if guild_id is not None else None
        if session is None or session.ttsq is None:
            await interaction.followup.send("Nu sunt în voice.", ephemeral=True)
            return
        try:
            session.ttsq.clear()
            log.info("voice stop: TTS queue cleared by user %s", interaction.user)
        except Exception as e:
            log.warning("voice stop: ttsq.clear failed: %s", e)
            await interaction.followup.send(f"Eroare la oprire: {e}", ephemeral=True)
            return
        await interaction.followup.send("Audio oprit.", ephemeral=True)

    @voice_group.command(name="doctor", description="Verifică voice stack")
    async def doctor(interaction: discord.Interaction) -> None:
        await interaction.response.defer(ephemeral=True)
        checks: list[tuple[str, bool]] = []
        # libopus
        try:
            checks.append(("libopus", bool(discord.opus.is_loaded())))
        except Exception:
            checks.append(("libopus", False))
        # warmup
        checks.append(("voice load error", _voice_load_error is None))
        # Build response
        lines = ["**Voice doctor:**"]
        for label, ok in checks:
            lines.append(f"{'OK' if ok else 'FAIL'} — {label}")
        if _voice_load_error:
            lines.append(f"  details: {_voice_load_error}")
        await interaction.followup.send("\n".join(lines), ephemeral=True)

    # --- /voice mirror on|off ---
    mirror_group = app_commands.Group(
        name="mirror", description="Text mirror", parent=voice_group
    )

    @mirror_group.command(name="on", description="Activează text mirror în canal")
    async def mirror_on(interaction: discord.Interaction) -> None:
        await interaction.response.defer(ephemeral=True)
        guild_id = interaction.guild.id if interaction.guild else None
        s = _voice_sessions.get(guild_id) if guild_id is not None else None
        if s is None:
            await interaction.followup.send("Nu sunt în voice.", ephemeral=True)
            return
        s.mirror_enabled = True
        await interaction.followup.send("Mirror ON.", ephemeral=True)

    @mirror_group.command(name="off", description="Dezactivează text mirror")
    async def mirror_off(interaction: discord.Interaction) -> None:
        await interaction.response.defer(ephemeral=True)
        guild_id = interaction.guild.id if interaction.guild else None
        s = _voice_sessions.get(guild_id) if guild_id is not None else None
        if s is None:
            await interaction.followup.send("Nu sunt în voice.", ephemeral=True)
            return
        s.mirror_enabled = False
        await interaction.followup.send("Mirror OFF.", ephemeral=True)

    # --- /voice record on|off ---
    record_group = app_commands.Group(
        name="record", description="KB recording", parent=voice_group
    )

    @record_group.command(name="on", description="Activează înregistrare în KB")
    async def record_on(interaction: discord.Interaction) -> None:
        await interaction.response.defer(ephemeral=True)
        guild_id = interaction.guild.id if interaction.guild else None
        s = _voice_sessions.get(guild_id) if guild_id is not None else None
        if s is None:
            await interaction.followup.send("Nu sunt în voice.", ephemeral=True)
            return
        s.record_enabled = True
        await interaction.followup.send("Record ON.", ephemeral=True)

    @record_group.command(name="off", description="Dezactivează înregistrare")
    async def record_off(interaction: discord.Interaction) -> None:
        await interaction.response.defer(ephemeral=True)
        guild_id = interaction.guild.id if interaction.guild else None
        s = _voice_sessions.get(guild_id) if guild_id is not None else None
        if s is None:
            await interaction.followup.send("Nu sunt în voice.", ephemeral=True)
            return
        s.record_enabled = False
        await interaction.followup.send("Record OFF.", ephemeral=True)

    return voice_group
