"""Voice endpoints — Gemini interaction, local TTS, prompt management."""

from __future__ import annotations

import asyncio

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from Project.Sanad.core.config_loader import section as _cfg_section
from Project.Sanad.core.logger import get_logger

log = get_logger("voice_route")

router = APIRouter()

_VR = _cfg_section("dashboard", "voice_route")
_API = _cfg_section("dashboard", "api_input")
# MAX_TEXT_LEN — SINGLE SOURCE in dashboard.api_input
MAX_TEXT_LEN = _API.get("max_text_len", 2000)
_API_KEY_MASK_VISIBLE = _VR.get("api_key_mask_visible", 4)


def _mask_api_key(key: str) -> str:
    """Mask an API key for display — keeps 4 chars on each end.

    Examples:
      ""               → ""
      "AIza123"        → "*******"        (≤8 chars = full mask)
      "AIzaSy...kqf8"  → "AIza***...kqf8" (>8 chars = partial mask)
    """
    if not key:
        return ""
    if len(key) <= 8:
        return "*" * len(key)
    return f"{key[:4]}{'*' * (len(key) - 8)}{key[-4:]}"


class TextPayload(BaseModel):
    text: str
    engine: str = "gemini"  # "gemini" | "local"


@router.get("/status")
async def voice_status():
    from Project.Sanad.main import voice_client, local_tts
    return {
        "gemini": voice_client.status() if voice_client else {},
        "local_tts": local_tts.status() if local_tts else {},
    }


@router.post("/generate")
async def generate_speech(payload: TextPayload):
    """Generate speech from text using Gemini or local TTS."""
    if not payload.text.strip():
        raise HTTPException(400, "Text cannot be empty.")
    if len(payload.text) > MAX_TEXT_LEN:
        raise HTTPException(413, f"Text too long (max {MAX_TEXT_LEN} chars).")

    from Project.Sanad.main import voice_client, local_tts, audio_mgr

    if payload.engine == "local":
        if local_tts is None:
            raise HTTPException(503, "Local TTS not available.")
        pcm = await asyncio.to_thread(local_tts.synthesize, payload.text)
        if audio_mgr:
            await asyncio.to_thread(audio_mgr.play_pcm, pcm, 1, 16000, 2)
        return {
            "ok": True,
            "engine": "local",
            "duration_sec": round(len(pcm) / (16000 * 2), 3),
        }
    else:
        if voice_client is None:
            raise HTTPException(503, "Voice client not initialized.")
        if not voice_client.connected:
            try:
                await voice_client.connect()
            except Exception:
                log.exception("Gemini reconnect failed in /generate")
                raise HTTPException(503, "Gemini not connected and reconnect failed.")
        # Check session ownership — TypedReplay or live loop may hold it
        if voice_client.session_owner is not None:
            raise HTTPException(
                409,
                f"Voice session busy (owned by {voice_client.session_owner})",
            )
        try:
            audio_bytes, text_parts = await voice_client.send_text(
                payload.text, owner="voice_route"
            )
        except RuntimeError as exc:
            raise HTTPException(503, str(exc))
        except Exception as exc:
            raise HTTPException(502, f"Gemini communication error: {exc}")
        if audio_bytes and audio_mgr:
            await asyncio.to_thread(audio_mgr.play_pcm, audio_bytes, 1, 24000, 2)
        return {
            "ok": True,
            "engine": "gemini",
            "has_audio": bool(audio_bytes),
            "text_response": text_parts,
        }


@router.post("/connect")
async def connect_gemini():
    from Project.Sanad.main import voice_client
    if voice_client is None:
        raise HTTPException(503, "Voice client not initialized.")
    try:
        await voice_client.connect()
    except Exception as exc:
        raise HTTPException(502, f"Gemini connection failed: {exc}")
    return {"connected": voice_client.connected}


@router.post("/disconnect")
async def disconnect_gemini():
    from Project.Sanad.main import voice_client
    if voice_client:
        await voice_client.disconnect()
    return {"connected": False}


# ─────────────────────── Gemini API key management ───────────────────────

class ApiKeyPayload(BaseModel):
    api_key: str


@router.get("/api-key")
async def get_api_key():
    """Return the current Gemini API key in masked form.

    Never returns the full key. Response:
      {
        "has_key": true,
        "masked":  "AIza***...kqf8",
        "length":  39,
        "source":  "config_file" | "default"
      }
    """
    import Project.Sanad.config as cfg_mod
    key = getattr(cfg_mod, "GEMINI_API_KEY", "") or ""
    # Detect where the value came from (persisted override vs module default)
    try:
        from Project.Sanad.config import load_config
        stored = load_config().get("gemini", {}) or {}
        source = "config_file" if stored.get("api_key") else "default"
    except Exception:
        source = "default"
    return {
        "has_key": bool(key),
        "masked": _mask_api_key(key),
        "length": len(key),
        "source": source,
    }


@router.post("/api-key")
async def update_api_key(payload: ApiKeyPayload):
    """Update the Gemini API key — persists to data/motions/config.json and
    hot-swaps the in-memory value so the next Gemini connect uses it.

    Also disconnects any currently-connected Gemini session so that the
    next reconnect picks up the new key cleanly. Returns the NEW masked
    key + a flag telling the dashboard to trigger a reconnect.
    """
    key = payload.api_key.strip()
    if not key:
        raise HTTPException(400, "API key cannot be empty.")
    if len(key) < 20:
        raise HTTPException(400, "API key looks too short.")
    if not key.startswith("AIza"):
        raise HTTPException(
            400,
            "Gemini API keys normally start with 'AIza'. "
            "Double-check you're pasting a Google AI Studio key.",
        )

    # Persist to data/motions/config.json (atomic temp-then-replace)
    try:
        from Project.Sanad.config import load_config, save_config
        cfg = load_config() or {}
        gemini_cfg = cfg.get("gemini") if isinstance(cfg.get("gemini"), dict) else {}
        gemini_cfg["api_key"] = key
        cfg["gemini"] = gemini_cfg
        save_config(cfg)
    except Exception as exc:
        log.exception("Failed to persist API key to config.json")
        raise HTTPException(500, f"Could not save config: {exc}")

    # Hot-swap the in-memory module globals.
    # Both Project.Sanad.config AND Project.Sanad.gemini.client
    # have their OWN reference to GEMINI_API_KEY (the latter was created
    # at `from Project.Sanad.config import GEMINI_API_KEY` at import time).
    # Python's `from X import Y` binds a local name — updating config.Y
    # alone does NOT propagate to the importer, so we must patch both.
    try:
        import Project.Sanad.config as _cfg_mod
        _cfg_mod.GEMINI_API_KEY = key
    except Exception:
        log.exception("could not patch config.GEMINI_API_KEY")

    try:
        import Project.Sanad.gemini.client as _gc
        _gc.GEMINI_API_KEY = key
    except Exception:
        log.exception("could not patch gemini.client.GEMINI_API_KEY")

    # Disconnect any live session so reconnect uses the new key.
    from Project.Sanad.main import voice_client
    was_connected = False
    if voice_client is not None:
        was_connected = bool(getattr(voice_client, "connected", False))
        if was_connected:
            try:
                await voice_client.disconnect()
            except Exception:
                log.exception("disconnect during api-key swap failed")

    log.info("Gemini API key updated (length=%d) source=config_file", len(key))

    return {
        "ok": True,
        "masked": _mask_api_key(key),
        "length": len(key),
        "source": "config_file",
        "was_connected": was_connected,
        "message": (
            "API key saved. Click 'Connect' to reopen the Gemini session with "
            "the new key. Any running Live Gemini subprocess must be restarted "
            "separately (Stop → Start) to pick up the new key."
        ),
    }
