"""
routers/tavus.py — Tavus CVI live video roleplay integration
Endpoint: POST /api/tavus/start-session

All credentials come from config.py / .env — never hardcoded here.
"""
from __future__ import annotations
import logging

import httpx
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse

from config import settings
from services.auth import get_current_user
# Note: services.store is intentionally NOT imported. Scenarios for
# the role-play module come from the People Hub external API and are
# passed inline in the request body — we never consult the local
# data/scenarios.json store from the Tavus routes.

log = logging.getLogger(__name__)
router = APIRouter(prefix="/api/tavus", tags=["tavus"])


# Lowercase shim so legacy code (`_s.tavus_api_key`, etc.) keeps working.
# Each property also sanitises the raw env value — operators frequently
# paste the key with surrounding quotes or a trailing newline / space,
# both of which Tavus rejects with "Invalid access token" without
# telling you which character is wrong. Stripping here eliminates the
# entire class of "I copied it from the dashboard but it doesn't work"
# bugs.
def _clean_env(raw: str) -> str:
    if not raw:
        return ""
    s = raw.strip()
    # Strip a single pair of matching surrounding quotes (single or
    # double). dotenv libraries usually do this, but our config.py
    # uses os.getenv directly, so we re-apply the rule.
    if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"):
        s = s[1:-1].strip()
    return s


class _TavusSettings:
    @property
    def tavus_api_key(self):    return _clean_env(settings.TAVUS_API_KEY)
    @property
    def tavus_base_url(self):   return _clean_env(settings.TAVUS_BASE_URL) or "https://tavusapi.com/v2"
    @property
    def tavus_replica_id(self): return _clean_env(settings.TAVUS_REPLICA_ID)
    @property
    def tavus_persona_id(self): return _clean_env(settings.TAVUS_PERSONA_ID)

_s = _TavusSettings()


def _pick(raw: dict, *keys: str, default: str = "") -> str:
    """Read the first non-empty value from any of the given dict keys.

    The People Hub external roleplay API returns scenarios in a loose
    snake_case-or-camelCase shape — we accept both styles here so the
    Tavus persona prompt comes out the same regardless of which
    convention the upstream uses for a given field.
    """
    for k in keys:
        v = raw.get(k)
        if v not in (None, ""):
            return str(v)
    return default


def _build_system_prompt(raw: dict, candidate_name: str = "") -> str:
    """Map an external roleplay scenario (raw dict from the People Hub
    API) directly into the Tavus persona system prompt.

    Everything we need lives on the dict already — we do NOT need a
    Pydantic Scenario object, and we do NOT need to look anything up
    in the local data/scenarios.json store. The same dict shape is
    what the lobby renders and what voice / video role play already
    consume, so passing it straight through guarantees the Live AI
    flow uses the same scenario the candidate actually picked.

    ``candidate_name`` is the candidate's display name (from the
    People Hub login envelope, stashed on the frontend). When
    provided, it is woven into the prompt so the AI opens with
    "Hi <name>, …" instead of asking the candidate to type their
    name — that was the "the AI keeps asking who I am" bug we saw
    in user testing.
    """
    ai_character   = _pick(raw, "ai_character", "aiCharacter", "character", "partner", "npc", default="AI Character")
    context        = _pick(raw, "context", "scene", "background", "description", default="Have a realistic conversation.")
    ai_personality = _pick(raw, "ai_personality", "aiPersonality", "personality", default="Be realistic and challenging. Stay fully in character.")
    learner_role   = _pick(raw, "learner_role", "learnerRole", "user_role", "userRole", default="Candidate")
    difficulty     = _pick(raw, "difficulty", "level", default="Intermediate")
    category       = _pick(raw, "cat_label", "catLabel", "category_label", "categoryLabel", "category", default="General")
    language_name  = _pick(raw, "language_name", "languageName", "lang_name", "langName", default="English")
    turns          = _pick(raw, "turns", "max_turns", "maxTurns", default="8")

    # Greeting + identity directives — only emit when we actually
    # have a name to use, otherwise leave it out so the AI falls
    # back to its scenario-driven opener rather than greeting "Hi
    # Candidate, …".
    name = (candidate_name or "").strip()
    identity_block = ""
    if name:
        identity_block = (
            f"\n\nIMPORTANT — CANDIDATE IDENTITY:\n"
            f"- The candidate's name is {name}. They are the {learner_role}.\n"
            f"- Greet them BY NAME at the start (e.g. \"Hi {name}, ...\").\n"
            f"- Do NOT ask the candidate to introduce themselves or to "
            f"  state their name — you already know who they are.\n"
            f"- Do NOT ask what their role is — you already know they "
            f"  are playing the {learner_role}.\n"
            f"- Jump straight into the scenario in your very first turn."
        )

    return (
        f"You are {ai_character}, a character in a professional training roleplay.\n\n"
        f"SCENARIO:\n{context}{identity_block}\n\n"
        f"YOUR PERSONALITY & BEHAVIOUR:\n{ai_personality}\n\n"
        "RULES:\n"
        f"- You are speaking with someone playing the role of: {learner_role}\n"
        f"- Difficulty: {difficulty}\n"
        f"- Category: {category}\n"
        f"- Language: {language_name}\n"
        "- This is a training simulation — be realistic but constructive.\n"
        "- Never break character. Never reveal you are an AI unless directly asked.\n"
        "- Keep responses conversational and appropriately emotional for your character.\n"
        f"- Aim for {turns} conversation exchanges total."
    )


@router.post("/start-session")
async def start_tavus_session(body: dict, current_user=Depends(get_current_user)):
    """
    Start a Tavus CVI live video session for a scenario.
    Returns { conversation_url, conversation_id } to embed in the frontend iframe.

    Source of truth for the scenario is the request body.

    Scenarios for the role-play lobby come from the People Hub external
    API (NOT from data/scenarios.json), so we accept the full scenario
    object inline — same shape the lobby renders and that voice / video
    role play already consume. We do NOT consult the local JSON store
    here; if the client sends the scenario, we use it. This avoids the
    earlier 404s where data/scenarios.json had different ids than what
    the lobby was actually showing.

    Body shape:
        {
          "scenario": { ...full roleplay dict from the People Hub API... },
          "access_key": "<candidate access key>",   # optional, for logging
          "scenario_id": "<id>"                     # optional, for logging
        }
    """
    if not _s.tavus_api_key:
        raise HTTPException(
            503,
            detail=(
                "Tavus CVI is not configured. "
                "Set TAVUS_API_KEY in your .env file. "
                "See /tavus-guide for full setup instructions."
            ),
        )

    raw_scenario = body.get("scenario") or {}
    scenario_id  = (body.get("scenario_id") or "").strip()
    access_key   = (
        body.get("access_key")
        or (current_user or {}).get("access_key")
        or ""
    ).strip()
    # Candidate name source order:
    #   1. body.candidate_name   (sent inline by the frontend from the
    #      People Hub login envelope so the AI greets by name)
    #   2. current_user.name     (when JWT-authenticated)
    #   3. (no name)             — AI will fall back to its default opener
    candidate_name = (
        body.get("candidate_name")
        or (current_user or {}).get("name")
        or ""
    ).strip()

    if not isinstance(raw_scenario, dict) or not raw_scenario:
        # The frontend always sends scenario inline now. If we got here
        # without it, the candidate likely refreshed mid-flow and the
        # selectedScenario state was lost — ask them to reload the
        # lobby rather than guess from a stale store entry.
        raise HTTPException(
            422,
            detail=(
                "scenario object is required. Reload the lobby and "
                "pick a scenario again, then try Live AI Role Play."
            ),
        )

    # Per-scenario Tavus overrides (admins can pin a particular
    # replica/persona to a specific scenario in the People Hub admin
    # panel). Both snake_case and camelCase accepted.
    replica_id = (
        _pick(raw_scenario, "tavus_replica_id", "tavusReplicaId")
        or _s.tavus_replica_id
    )
    persona_id = (
        _pick(raw_scenario, "tavus_persona_id", "tavusPersonaId")
        or _s.tavus_persona_id
    )

    title = _pick(raw_scenario, "title", "name", default="Roleplay")[:60]

    # Defensive: require a replica_id. Tavus rejects requests without
    # one with a generic 400, which previously bubbled up as the 502
    # the candidate sees. Returning 503 here makes the misconfiguration
    # explicit to the operator without leaking secrets.
    if not replica_id:
        raise HTTPException(
            503,
            detail=(
                "Live AI Role Play is not fully configured: no Tavus "
                "replica id is set. Add TAVUS_REPLICA_ID to .env, or "
                "set tavus_replica_id on the scenario object."
            ),
        )

    # Keep the payload to the absolute minimum required fields. Earlier
    # versions sent a `properties` block (max_call_duration,
    # enable_recording, apply_greenscreen) — some Tavus plans reject
    # those when the account isn't on the right tier, and that 4xx
    # bubbled up to the candidate as a generic 502. The minimal shape
    # below is what Tavus's "Create a Conversation" endpoint actually
    # requires; extra niceties can be added back once we know the
    # account-tier requirements.
    payload: dict = {
        "replica_id":        replica_id,
        "conversation_name": f"RolePilot · {title}",
    }

    if persona_id:
        # Pre-created persona contains the system prompt — fastest path
        payload["persona_id"] = persona_id
    else:
        # Inline conversational context built directly from the raw
        # scenario dict (no Pydantic Scenario object, no local store).
        # Tavus caps this at a few thousand chars — clip defensively
        # so we never trigger a 413 / 400 from an oversized prompt.
        payload["conversational_context"] = _build_system_prompt(raw_scenario, candidate_name)[:3500]

    log.info(
        "[Tavus] starting session  ak=%s  scenario=%s  replica=%s  persona=%s  body=%s",
        (access_key[:6] + "…") if access_key else "(none)",
        scenario_id or _pick(raw_scenario, "id", default="(inline)"),
        replica_id, persona_id or "(inline)",
        # Full payload (replica + persona + clipped prompt) — useful
        # when Tavus rejects a request and we need to compare against
        # the dashboard's request inspector.
        {**payload, "conversational_context": payload.get("conversational_context", "")[:120] + ("…" if len(payload.get("conversational_context", "")) > 120 else "")},
    )

    # Tavus base URL must NOT have a trailing slash — the upstream
    # router treats `/conversations` and `//conversations` as separate
    # paths and the latter 404s.
    base = (_s.tavus_base_url or "https://tavusapi.com/v2").rstrip("/")
    url  = f"{base}/conversations"

    try:
        async with httpx.AsyncClient(timeout=20.0) as client:
            resp = await client.post(
                url,
                headers={
                    "x-api-key":    _s.tavus_api_key,
                    "Content-Type": "application/json",
                },
                json=payload,
            )
    except httpx.RequestError as exc:
        # Network failure / DNS / TLS — these never reach the upstream.
        log.error("[Tavus] network error talking to %s: %s", url, exc)
        raise HTTPException(
            502,
            detail=(
                f"Could not reach Tavus at {base}. "
                "Check your network connection and the TAVUS_BASE_URL "
                f"setting. Underlying error: {exc.__class__.__name__}: {exc}"
            ),
        )

    if resp.status_code not in (200, 201):
        # Surface the FULL Tavus response in both the log and the
        # 502 detail so the operator can see exactly why the request
        # was rejected. Common reasons:
        #   401  — invalid TAVUS_API_KEY
        #   402  — out of conversation minutes / plan exceeded
        #   404  — replica_id or persona_id not found on this account
        #   400  — payload shape rejected (bad persona_id, etc.)
        body_preview = resp.text[:600] if resp.text else "(empty body)"
        log.error(
            "[Tavus] API error %s at %s — replica=%s persona=%s body=%s",
            resp.status_code, url, replica_id, persona_id or "(inline)",
            body_preview,
        )
        hint = ""
        sc   = resp.status_code
        if sc == 401:
            hint = " Likely cause: TAVUS_API_KEY is wrong or revoked."
        elif sc == 402:
            hint = " Likely cause: your Tavus plan is out of conversation minutes."
        elif sc == 404:
            hint = " Likely cause: replica_id or persona_id doesn't exist on this Tavus account."
        elif sc == 400:
            hint = " Likely cause: request shape rejected — check that the replica_id is active."
        raise HTTPException(
            502,
            detail=(
                f"Tavus API returned HTTP {sc}.{hint} "
                f"Tavus response: {body_preview}"
            ),
        )

    try:
        data = resp.json()
    except Exception:  # noqa: BLE001
        log.error("[Tavus] response was not JSON: %s", resp.text[:400])
        raise HTTPException(502, detail="Tavus returned a non-JSON response.")

    conversation_url = data.get("conversation_url", "")
    conversation_id  = data.get("conversation_id", "")

    if not conversation_url:
        log.error("[Tavus] no conversation_url in response: %s", data)
        raise HTTPException(
            502,
            detail=(
                "Tavus accepted the request but did not return a "
                "conversation_url. Response keys: "
                f"{list(data.keys()) if isinstance(data, dict) else type(data).__name__}"
            ),
        )

    log.info("[Tavus] session started  conversation_id=%s", conversation_id)
    return JSONResponse({"conversation_url": conversation_url, "conversation_id": conversation_id})


@router.post("/create-persona")
async def create_tavus_persona(body: dict, _=Depends(get_current_user)):
    """
    Pre-create a Tavus persona for a scenario.
    Call this from the admin panel when publishing a scenario to speed up
    future video session starts (persona carries the system prompt).

    Accepts the scenario inline in the request body — same shape as
    /start-session — so admins paste the People Hub scenario JSON and
    don't depend on the local data/scenarios.json being up-to-date.
    A legacy scenario_id is also accepted; when given it is logged
    only (no lookup) so the persona_name reflects what the admin sees.
    """
    if not _s.tavus_api_key:
        raise HTTPException(503, detail="TAVUS_API_KEY not configured")

    raw_scenario = body.get("scenario") or {}
    scenario_id  = (body.get("scenario_id") or "").strip()

    if not isinstance(raw_scenario, dict) or not raw_scenario:
        raise HTTPException(422, detail="scenario object is required")

    system_prompt = _build_system_prompt(raw_scenario)
    persona_name  = _pick(raw_scenario, "title", "name", default="Roleplay")[:80]

    async with httpx.AsyncClient(timeout=20.0) as client:
        resp = await client.post(
            f"{_s.tavus_base_url}/personas",
            headers={
                "x-api-key": _s.tavus_api_key,
                "Content-Type": "application/json",
            },
            json={
                "persona_name": persona_name,
                "system_prompt": system_prompt,
                "pipeline_mode": "full",
            },
        )

    if resp.status_code not in (200, 201):
        raise HTTPException(
            502,
            detail=f"Tavus persona creation failed: HTTP {resp.status_code} — {resp.text[:200]}",
        )

    persona_id = resp.json().get("persona_id", "")
    if persona_id:
        # We no longer persist the persona_id locally — admins should
        # save it back to the People Hub admin panel (which is the
        # source of truth for scenarios) so future Live AI sessions
        # pick it up via the `tavus_persona_id` / `tavusPersonaId`
        # field on the scenario object.
        log.info(
            "[Tavus] persona created  persona_id=%s  scenario=%s",
            persona_id, scenario_id or persona_name,
        )

    return {
        "persona_id":  persona_id,
        "scenario_id": scenario_id,
        "persona_name": persona_name,
    }


@router.get("/health")
async def tavus_health():
    """Diagnostic endpoint — verifies the configured TAVUS_API_KEY is
    accepted by Tavus, without starting a (billable) conversation.
    Returns a JSON status so an operator can curl this from the box:

        curl http://localhost:8000/api/tavus/health

    Status meanings:
      • ok=true  → key is valid, replicas list reachable
      • ok=false → field `detail` explains what went wrong

    Intentionally public (no auth dep) because the whole point is to
    let operators debug misconfiguration before any user can log in.
    The response NEVER leaks the API key itself — only its length and
    first/last few characters are echoed so the operator can confirm
    the .env value matches the Tavus dashboard without a copy-paste
    mistake.
    """
    key = _s.tavus_api_key
    base = (_s.tavus_base_url or "https://tavusapi.com/v2").rstrip("/")

    if not key:
        return JSONResponse(
            status_code=503,
            content={
                "ok":     False,
                "detail": "TAVUS_API_KEY is empty. Set it in .env and restart the server.",
                "base":   base,
            },
        )

    # Redacted key fingerprint — useful when the operator updated .env
    # but the server still has the old value cached. Format is
    # `<first4>…<last4> (len=<n>)` so the dashboard can be cross-
    # checked without exposing the secret.
    if len(key) >= 8:
        key_fp = f"{key[:4]}…{key[-4:]} (len={len(key)})"
    else:
        key_fp = f"(len={len(key)}, too short to redact safely)"

    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            # Listing replicas is a free read-only call — perfect for
            # auth verification. If the key is valid we get 200 + a
            # list; if not we get 401 with the same "Invalid access
            # token" body the conversations endpoint returns.
            resp = await client.get(
                f"{base}/replicas",
                headers={"x-api-key": key},
            )
    except httpx.RequestError as exc:
        return JSONResponse(
            status_code=502,
            content={
                "ok":     False,
                "detail": f"Could not reach Tavus at {base}: {exc.__class__.__name__}: {exc}",
                "key_fp": key_fp,
                "base":   base,
            },
        )

    sc = resp.status_code
    body_preview = resp.text[:300] if resp.text else "(empty body)"

    if sc == 200:
        # Don't echo the actual replica list — it can be large. Just
        # confirm the count.
        replicas = []
        try:
            data = resp.json()
            if isinstance(data, dict) and isinstance(data.get("data"), list):
                replicas = data["data"]
            elif isinstance(data, list):
                replicas = data
        except Exception:  # noqa: BLE001
            pass
        return {
            "ok":             True,
            "detail":         "TAVUS_API_KEY is valid.",
            "key_fp":         key_fp,
            "base":           base,
            "replica_count":  len(replicas),
            "configured_replica_id": _s.tavus_replica_id or "(not set)",
            "configured_persona_id": _s.tavus_persona_id or "(not set)",
        }

    hint = ""
    if sc == 401:
        hint = (
            " — Tavus rejected this exact key. Double-check the value "
            "in .env against the Tavus dashboard, ensure there are no "
            "surrounding quotes / whitespace, and RESTART the server "
            "after editing .env (Python doesn't hot-reload env vars)."
        )
    elif sc == 403:
        hint = " — the key is recognised but lacks permission for /replicas. Check the Tavus plan tier."

    return JSONResponse(
        status_code=502,
        content={
            "ok":     False,
            "detail": f"Tavus returned HTTP {sc}{hint} Body: {body_preview}",
            "key_fp": key_fp,
            "base":   base,
        },
    )


@router.post("/webhook")
async def tavus_webhook(payload: dict):
    """
    Receive Tavus webhook events (conversation.ended, transcript_ready).
    Register this URL in your Tavus dashboard:
      POST https://your-domain.com/api/tavus/webhook
    """
    event           = payload.get("event", "")
    conversation_id = payload.get("conversation_id", "")
    log.info("[Tavus] webhook  event=%s  conversation_id=%s", event, conversation_id)

    if event == "conversation.ended":
        transcript = payload.get("transcript", [])
        duration   = payload.get("duration_seconds", 0)
        log.info(
            "[Tavus] session ended  turns=%d  duration=%ds",
            len(transcript), duration,
        )
        # TODO: look up session by tavus_conversation_id, store transcript,
        # call generate_report_analysis, persist report.

    return {"ok": True}
