"""
agent/ai_agent.py — AI Voice Agent v2
Features:
- MySQL vector search (no ChromaDB/Qdrant)
- ElevenLabs OR Google TTS (switchable from dashboard)
- Auto language detection (English, Hindi, Swahili, Chinese)
- Email transcript after call
Start: uvicorn ai_agent:app --host 127.0.0.1 --port 8000
"""

import os, json, time, base64, asyncio, tempfile, logging, smtplib
from pathlib import Path
from typing import Optional
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import numpy as np

try:
    from dotenv import load_dotenv
    load_dotenv(Path(__file__).parent / ".env")
    print("✓ .env loaded")
except ImportError:
    pass

from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, Form, HTTPException, Header
from openai import AsyncOpenAI
from deepgram import DeepgramClient, LiveTranscriptionEvents, LiveOptions
from elevenlabs.client import ElevenLabs
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
import mysql.connector

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
log = logging.getLogger("voice_agent")

def req(k):
    v = os.environ.get(k, "").strip()
    if not v: raise RuntimeError(f"Missing .env: {k}")
    return v

OPENAI_KEY       = req("OPENAI_API_KEY")
DEEPGRAM_KEY     = req("DEEPGRAM_API_KEY")
ELEVENLABS_KEY   = req("ELEVENLABS_API_KEY")
ELEVENLABS_VOICE = os.environ.get("ELEVENLABS_VOICE_ID", "EXAVITQu4vr4xnSDxMaL")
AGENT_SECRET     = os.environ.get("AI_AGENT_KEY", "local_dev_secret_key_change_me")
DB_HOST          = req("DB_HOST")
DB_PORT          = int(os.environ.get("DB_PORT", "3306"))
DB_USER          = req("DB_USER")
DB_PASS          = req("DB_PASS")
DB_NAME          = req("DB_NAME")
LLM_MODEL        = os.environ.get("LLM_MODEL", "gpt-4o")
SMTP_HOST        = os.environ.get("SMTP_HOST", "smtp.gmail.com")
SMTP_PORT        = int(os.environ.get("SMTP_PORT", "587"))
SMTP_USER        = os.environ.get("SMTP_USER", "")
SMTP_PASS        = os.environ.get("SMTP_PASS", "")

oai = AsyncOpenAI(api_key=OPENAI_KEY)
el  = ElevenLabs(api_key=ELEVENLABS_KEY)
app = FastAPI(title="AI Voice Agent v2")

# ── Language config ──────────────────────────────────────────────────────
LANGUAGE_MAP = {
    "en": {"name": "English",  "deepgram": "en-US",  "google_code": "en-US",  "google_voice": "en-US-Neural2-F"},
    "hi": {"name": "Hindi",    "deepgram": "hi",     "google_code": "hi-IN",  "google_voice": "hi-IN-Neural2-A"},
    "sw": {"name": "Swahili",  "deepgram": "sw",     "google_code": "sw-KE",  "google_voice": "sw-KE-Standard-A"},
    "zh": {"name": "Chinese",  "deepgram": "zh-CN",  "google_code": "cmn-CN", "google_voice": "cmn-CN-Wavenet-A"},
}

# ── DB ───────────────────────────────────────────────────────────────────
def db_exec(sql: str, params: tuple = ()) -> list:
    conn = mysql.connector.connect(
        host=DB_HOST, port=DB_PORT, user=DB_USER,
        password=DB_PASS, database=DB_NAME,
        connection_timeout=10, ssl_disabled=True,
        consume_results=True
    )
    cur = conn.cursor(dictionary=True, buffered=True)
    cur.execute(sql, params)
    conn.commit()
    rows = cur.fetchall() if cur.description else []
    cur.close(); conn.close()
    return rows

def get_config(key: str, default: str = "") -> str:
    try:
        rows = db_exec("SELECT config_value FROM agent_config WHERE config_key=%s", (key,))
        return rows[0]["config_value"] if rows else default
    except:
        return default

# ── Embeddings ───────────────────────────────────────────────────────────
async def get_embedding(text: str) -> list:
    resp = await oai.embeddings.create(
        model="text-embedding-3-small",
        input=text.replace("\n", " ")
    )
    return resp.data[0].embedding

async def get_embeddings_batch(texts: list) -> list:
    all_vecs = []
    for i in range(0, len(texts), 100):
        resp = await oai.embeddings.create(
            model="text-embedding-3-small",
            input=[t.replace("\n", " ") for t in texts[i:i+100]]
        )
        all_vecs.extend([x.embedding for x in resp.data])
    return all_vecs

# ── RAG ──────────────────────────────────────────────────────────────────
async def retrieve_context(query: str, top_n: int = 4) -> str:
    try:
        rows = db_exec("SELECT content, embedding FROM kb_chunks WHERE embedding IS NOT NULL")
        if not rows: return ""
        q_vec = np.array(await get_embedding(query), dtype=np.float32)
        scored = []
        for row in rows:
            try:
                cv    = np.array(json.loads(row["embedding"]), dtype=np.float32)
                denom = np.linalg.norm(q_vec) * np.linalg.norm(cv)
                score = float(np.dot(q_vec, cv) / denom) if denom > 0 else 0.0
                scored.append((score, row["content"]))
            except: continue
        scored.sort(key=lambda x: x[0], reverse=True)
        if scored: log.info(f"RAG scores: {[round(s,3) for s,_ in scored[:4]]}")
        relevant = [(s,t) for s,t in scored if s > 0.25]
        if not relevant:
            log.warning("RAG: no chunks above threshold")
            return ""
        return "\n\n".join(t for _,t in relevant[:top_n])
    except Exception as e:
        log.error(f"RAG error: {e}")
        return ""

# ── Language Detection ───────────────────────────────────────────────────
async def detect_language(text: str) -> str:
    """Use GPT to detect language of caller's speech."""
    try:
        resp = await oai.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{
                "role": "user",
                "content": f"Detect the language of this text and reply with ONLY the 2-letter code: en, hi, sw, or zh. Text: '{text}'"
            }],
            max_tokens=5,
            temperature=0
        )
        lang = resp.choices[0].message.content.strip().lower()[:2]
        return lang if lang in LANGUAGE_MAP else "en"
    except:
        return "en"

# ── LLM ──────────────────────────────────────────────────────────────────
async def generate_answer(transcript: str, context: str, lang: str = "en") -> str:
    agent_name  = get_config("agent_name", "AI Assistant")
    fallback    = get_config("fallback_message", "I'm sorry, I don't have that information.")
    temperature = float(get_config("llm_temperature", "0.3"))
    lang_name   = LANGUAGE_MAP.get(lang, {}).get("name", "English")

    system = f"""You are {agent_name}, a voice assistant answering phone calls.
Answer ONLY using the CONTEXT below. If not found say: "{fallback}"
IMPORTANT: Always respond in {lang_name}. If the caller speaks Hindi, reply in Hindi. If Swahili, reply in Swahili. If Chinese, reply in Chinese. If English, reply in English.
Max 2 sentences. No bullet points or markdown.
CONTEXT: {context or '(empty)'}"""

    try:
        resp = await oai.chat.completions.create(
            model=LLM_MODEL,
            messages=[{"role":"system","content":system},{"role":"user","content":transcript}],
            temperature=temperature, max_tokens=150
        )
        return resp.choices[0].message.content.strip()
    except Exception as e:
        log.error(f"LLM error: {e}")
        return "I am having a technical issue. Please try again."

# ── TTS: ElevenLabs ──────────────────────────────────────────────────────
def synthesize_elevenlabs(text: str, voice_id: str) -> bytes:
    try:
        audio = el.text_to_speech.convert(
            text=text,
            voice_id=voice_id,
            model_id="eleven_turbo_v2",
            output_format="ulaw_8000"
        )
        return b"".join(audio)
    except Exception as e:
        log.error(f"ElevenLabs TTS error: {e}")
        return b""

# ── TTS: Google ──────────────────────────────────────────────────────────
def synthesize_google(text: str, lang: str = "en") -> bytes:
    try:
        import urllib.request
        lang_cfg  = LANGUAGE_MAP.get(lang, LANGUAGE_MAP["en"])
        voice_name = lang_cfg["google_voice"]
        lang_code  = lang_cfg["google_code"]

        payload = json.dumps({
            "input":       {"text": text},
            "voice":       {"languageCode": lang_code, "name": voice_name},
            "audioConfig": {"audioEncoding": "MULAW", "sampleRateHertz": 8000}
        }).encode()

        google_key = os.environ.get("GOOGLE_TTS_KEY", "")
        url = f"https://texttospeech.googleapis.com/v1/text:synthesize?key={google_key}"

        req_obj = urllib.request.Request(
            url, data=payload,
            headers={"Content-Type": "application/json"},
            method="POST"
        )
        with urllib.request.urlopen(req_obj, timeout=10) as resp:
            result = json.loads(resp.read())
            return base64.b64decode(result["audioContent"])
    except Exception as e:
        log.error(f"Google TTS error: {e}")
        return b""

# ── TTS Router ───────────────────────────────────────────────────────────
def synthesize(text: str, lang: str = "en") -> bytes:
    engine   = get_config("tts_engine", "elevenlabs")
    voice_id = get_config("elevenlabs_voice_id", ELEVENLABS_VOICE)
    log.info(f"TTS engine: {engine}, lang: {lang}")
    if engine == "google":
        return synthesize_google(text, lang)
    else:
        return synthesize_elevenlabs(text, voice_id)

# ── Email transcript ──────────────────────────────────────────────────────
def send_transcript_email(call_sid: str, caller: str, turns: list):
    try:
        if not SMTP_USER or not SMTP_PASS:
            log.warning("SMTP not configured — skipping email")
            return
        email_to = get_config("transcript_email", "")
        if not email_to:
            return

        body = f"Call Transcript\n{'='*50}\n"
        body += f"Call SID: {call_sid}\nCaller: {caller}\nTime: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
        for t in turns:
            body += f"Caller: {t.get('caller_said','')}\n"
            body += f"Agent:  {t.get('agent_replied','')}\n\n"

        msg = MIMEMultipart()
        msg["From"]    = SMTP_USER
        msg["To"]      = email_to
        msg["Subject"] = f"Call Transcript — {caller} — {time.strftime('%Y-%m-%d %H:%M')}"
        msg.attach(MIMEText(body, "plain"))

        with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
            server.starttls()
            server.login(SMTP_USER, SMTP_PASS)
            server.send_message(msg)

        db_exec("UPDATE call_logs SET transcript_emailed=1 WHERE call_sid=%s", (call_sid,))
        log.info(f"Transcript email sent to {email_to}")
    except Exception as e:
        log.error(f"Email error: {e}")

# ── WebSocket ─────────────────────────────────────────────────────────────
@app.websocket("/ws/audio")
async def audio_ws(ws: WebSocket):
    await ws.accept()
    call_sid   = ws.query_params.get("call_sid", "unknown")
    turn_index = 0
    caller_num = "unknown"
    all_turns  = []
    detected_lang = "en"
    stream_sid = call_sid
    log.info(f"[{call_sid}] Call connected")

    try:
        rows = db_exec("SELECT caller_number FROM call_logs WHERE call_sid=%s", (call_sid,))
        if rows: caller_num = rows[0]["caller_number"]
        db_exec("UPDATE call_logs SET status='in-progress' WHERE call_sid=%s", (call_sid,))
    except: pass

    loop  = asyncio.get_event_loop()
    queue = asyncio.Queue()

    dg      = DeepgramClient(DEEPGRAM_KEY)
    dg_conn = dg.listen.live.v("1")

    def on_message(self_ref, result, **kwargs):
        try:
            alt = result.channel.alternatives[0]
            if result.is_final and alt.transcript.strip():
                loop.call_soon_threadsafe(queue.put_nowait, alt.transcript.strip())
        except Exception as e:
            log.warning(f"Transcript error: {e}")

    def on_error(self_ref, error, **kwargs):
        log.error(f"Deepgram error: {error}")

    dg_conn.on(LiveTranscriptionEvents.Transcript, on_message)
    dg_conn.on(LiveTranscriptionEvents.Error, on_error)

    # Detect language for Deepgram
    auto_detect = get_config("auto_detect_language", "yes") == "yes"
    dg_lang     = "en-US"  # start with English, update after first turn

    started = dg_conn.start(LiveOptions(
        model="nova-2", language=dg_lang,
        smart_format=True, endpointing=500,
        encoding="mulaw", sample_rate=8000,
        channels=1, interim_results=False
    ))
    log.info(f"[{call_sid}] Deepgram started: {started}")

    async def process_queue():
        nonlocal turn_index, detected_lang
        while True:
            try:
                text = await asyncio.wait_for(queue.get(), timeout=0.5)
                log.info(f"[{call_sid}] Caller: {text}")
                t0 = time.time()

                # Detect language on first turn
                if auto_detect and turn_index == 0:
                    detected_lang = await detect_language(text)
                    log.info(f"[{call_sid}] Detected language: {detected_lang}")
                    try:
                        db_exec("UPDATE call_logs SET detected_language=%s WHERE call_sid=%s",
                                (detected_lang, call_sid))
                    except: pass

                context = await retrieve_context(text)
                answer  = await generate_answer(text, context, detected_lang)
                audio   = synthesize(answer, detected_lang)
                ms      = int((time.time() - t0) * 1000)

                log.info(f"[{call_sid}] Agent ({ms}ms): {answer}")

                turn = {"caller_said": text, "agent_replied": answer}
                all_turns.append(turn)

                try:
                    db_exec("""INSERT INTO conversation_turns
                        (call_sid,turn_index,caller_said,agent_replied,latency_ms)
                        VALUES(%s,%s,%s,%s,%s)""",
                        (call_sid, turn_index, text, answer, ms))
                    turn_index += 1
                except Exception as e:
                    log.warning(f"DB error: {e}")

                if audio:
                    await ws.send_text(json.dumps({
                        "event": "media", "streamSid": stream_sid,
                        "media": {"payload": base64.b64encode(audio).decode()}
                    }))
            except asyncio.TimeoutError:
                continue
            except asyncio.CancelledError:
                break
            except Exception as e:
                log.error(f"Process error: {e}")

    processor = asyncio.create_task(process_queue())

    try:
        async for raw in ws.iter_text():
            try:
                data  = json.loads(raw)
                event = data.get("event", "")
                if event == "start":
                    stream_sid = data.get("streamSid", call_sid)
                    log.info(f"[{call_sid}] Stream started: {stream_sid}")
                elif event == "media":
                    dg_conn.send(base64.b64decode(data["media"]["payload"]))
                elif event == "stop":
                    log.info(f"[{call_sid}] Call ended")
                    break
            except Exception as e:
                log.warning(f"Message error: {e}")
    except WebSocketDisconnect:
        log.info(f"[{call_sid}] Disconnected")
    finally:
        processor.cancel()
        try: dg_conn.finish()
        except: pass

        # Send email transcript if enabled
        if get_config("email_transcripts", "no") == "yes" and all_turns:
            send_transcript_email(call_sid, caller_num, all_turns)

        log.info(f"[{call_sid}] Cleaned up — {turn_index} turns")

# ── Embed helpers ─────────────────────────────────────────────────────────
async def store_chunks(doc_id: int, texts: list):
    if not texts: raise ValueError("No chunks")
    log.info(f"Embedding {len(texts)} chunks...")
    vectors = await get_embeddings_batch(texts)
    for i, (text, vec) in enumerate(zip(texts, vectors)):
        db_exec("""INSERT INTO kb_chunks (document_id,chunk_index,content,embedding)
                   VALUES(%s,%s,%s,%s)""", (doc_id, i, text, json.dumps(vec)))
    db_exec("UPDATE kb_documents SET status='ready',chunk_count=%s WHERE id=%s",
            (len(texts), doc_id))
    log.info(f"✓ {len(texts)} chunks stored")

def split_docs(docs) -> list:
    sp = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=60)
    return [c.page_content for c in sp.split_documents(docs) if c.page_content.strip()]

def split_text(text: str) -> list:
    sp = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=60)
    return [c for c in sp.split_text(text) if c.strip()]

def check_key(k):
    if k != AGENT_SECRET: raise HTTPException(403, "Forbidden")

# ── Admin endpoints ───────────────────────────────────────────────────────
@app.post("/embed/pdf")
async def embed_pdf(file: UploadFile=File(...), title: str=Form(...),
                    doc_id: int=Form(...), x_agent_key: Optional[str]=Header(None)):
    check_key(x_agent_key)
    db_exec("UPDATE kb_documents SET status='processing' WHERE id=%s", (doc_id,))
    suffix = Path(file.filename).suffix if file.filename else ".pdf"
    with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
        tmp.write(await file.read()); path = tmp.name
    try:
        texts = split_docs(PyPDFLoader(path).load())
        await store_chunks(doc_id, texts)
        return {"ok": True, "chunks": len(texts)}
    except Exception as e:
        db_exec("UPDATE kb_documents SET status='error',error_msg=%s WHERE id=%s", (str(e), doc_id))
        raise HTTPException(500, str(e))
    finally:
        os.unlink(path)

@app.post("/embed/url")
async def embed_url(payload: dict, x_agent_key: Optional[str]=Header(None)):
    check_key(x_agent_key)
    url, doc_id = payload.get("url",""), payload.get("doc_id")
    if not url or not doc_id: raise HTTPException(400, "url and doc_id required")
    db_exec("UPDATE kb_documents SET status='processing' WHERE id=%s", (doc_id,))
    try:
        import httpx
        from bs4 import BeautifulSoup
        resp = httpx.get(url, timeout=30, follow_redirects=True,
                        headers={"User-Agent": "Mozilla/5.0"})
        soup = BeautifulSoup(resp.text, "lxml")
        for tag in soup(["nav","header","footer","script","style","aside","form"]):
            tag.decompose()
        clean  = soup.get_text(separator="\n", strip=True)
        lines  = [l.strip() for l in clean.splitlines() if len(l.strip()) > 40]
        texts  = split_text("\n".join(lines))
        await store_chunks(doc_id, texts)
        return {"ok": True, "chunks": len(texts)}
    except Exception as e:
        db_exec("UPDATE kb_documents SET status='error',error_msg=%s WHERE id=%s", (str(e), doc_id))
        raise HTTPException(500, str(e))

@app.post("/embed/text")
async def embed_text(payload: dict, x_agent_key: Optional[str]=Header(None)):
    check_key(x_agent_key)
    text, doc_id = payload.get("text",""), payload.get("doc_id")
    if not text or not doc_id: raise HTTPException(400, "text and doc_id required")
    db_exec("UPDATE kb_documents SET status='processing' WHERE id=%s", (doc_id,))
    try:
        texts = split_text(text)
        await store_chunks(doc_id, texts)
        return {"ok": True, "chunks": len(texts)}
    except Exception as e:
        db_exec("UPDATE kb_documents SET status='error',error_msg=%s WHERE id=%s", (str(e), doc_id))
        raise HTTPException(500, str(e))

@app.delete("/embed/{doc_id}")
async def delete_embed(doc_id: int, x_agent_key: Optional[str]=Header(None)):
    check_key(x_agent_key)
    db_exec("DELETE FROM kb_chunks WHERE document_id=%s", (doc_id,))
    return {"ok": True}

@app.get("/health")
def health():
    try: db_exec("SELECT 1"); db_ok = True; db_err = None
    except Exception as e: db_ok = False; db_err = str(e)
    try:
        rows  = db_exec("SELECT COUNT(*) AS n FROM kb_chunks WHERE embedding IS NOT NULL")
        chunks = rows[0]["n"] if rows else 0
    except: chunks = 0
    return {"status":"ok","db_connected":db_ok,"db_error":db_err,
            "chroma_chunks":chunks,"llm_model":LLM_MODEL,
            "tts_engine": get_config("tts_engine","elevenlabs"),
            "vector_engine":"mysql","elevenlabs_voice":ELEVENLABS_VOICE}

# ── WhatsApp text answer endpoint ────────────────────────────────────────
@app.post("/whatsapp/answer")
async def whatsapp_answer(payload: dict, x_agent_key: Optional[str]=Header(None)):
    """Called by PHP webhook to get AI answer for WhatsApp message."""
    check_key(x_agent_key)
    question = payload.get("question","").strip()
    phone    = payload.get("phone","")

    if not question:
        raise HTTPException(400, "question required")

    log.info(f"[WhatsApp] {phone}: {question}")

    # Detect language
    lang = await detect_language(question)
    log.info(f"[WhatsApp] Detected language: {lang}")

    # RAG search
    context = await retrieve_context(question)

    # Generate answer
    answer = await generate_answer(question, context, lang)
    log.info(f"[WhatsApp] Answer: {answer}")

    return {"answer": answer, "language": lang}

# ════════════════════════════════════════════════════════════════════════
#  WHATSAPP ENDPOINTS
# ════════════════════════════════════════════════════════════════════════

@app.post("/whatsapp/answer")
async def whatsapp_text(payload: dict, x_agent_key: Optional[str]=Header(None)):
    """Handle WhatsApp text message — detect language, RAG, LLM, reply."""
    check_key(x_agent_key)
    question = payload.get("question","").strip()
    phone    = payload.get("phone","")
    if not question: raise HTTPException(400,"question required")

    log.info(f"[WA-TEXT] {phone}: {question}")

    # Detect language
    lang = await detect_language(question)
    log.info(f"[WA-TEXT] Language: {lang}")

    # RAG + LLM
    context = await retrieve_context(question)
    answer  = await generate_answer(question, context, lang)
    log.info(f"[WA-TEXT] Answer: {answer}")

    # Log to DB
    try:
        db_exec("""INSERT INTO whatsapp_messages
            (message_sid,from_number,to_number,message_body,direction,status)
            VALUES(%s,%s,%s,%s,'inbound','processed')
            ON DUPLICATE KEY UPDATE status='processed'""",
            (f"api_{phone}_{int(time.time())}", phone, "agent", question))
    except: pass

    return {"answer": answer, "language": lang}


@app.post("/whatsapp/voice-note")
async def whatsapp_voice_note(
    audio: UploadFile = File(...),
    phone: str        = Form(...),
    x_agent_key: Optional[str] = Header(None)
):
    """Handle WhatsApp voice note — transcribe with Deepgram, then answer."""
    check_key(x_agent_key)
    log.info(f"[WA-VOICE] Voice note from {phone}")

    # Save audio to temp file
    suffix = Path(audio.filename).suffix if audio.filename else ".ogg"
    with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
        tmp.write(await audio.read())
        tmp_path = tmp.name

    try:
        # Transcribe with Deepgram (file-based, not streaming)
        dg = DeepgramClient(DEEPGRAM_KEY)
        with open(tmp_path, "rb") as f:
            audio_data = f.read()

        # Use Deepgram pre-recorded transcription
        import httpx
        headers = {"Authorization": f"Token {DEEPGRAM_KEY}",
                   "Content-Type": "audio/ogg"}
        params  = {"model": "nova-2", "smart_format": "true",
                   "detect_language": "true"}

        async with httpx.AsyncClient() as client:
            resp = await client.post(
                "https://api.deepgram.com/v1/listen",
                headers=headers,
                params=params,
                content=audio_data,
                timeout=30
            )

        if resp.status_code != 200:
            log.error(f"Deepgram transcription failed: {resp.status_code}")
            return {"transcript": "", "answer": "Sorry, could not transcribe voice note."}

        result     = resp.json()
        transcript = result["results"]["channels"][0]["alternatives"][0]["transcript"]
        detected   = result["results"]["channels"][0].get("detected_language", "en")
        lang       = detected[:2] if detected else "en"

        log.info(f"[WA-VOICE] Transcript: {transcript} (lang: {lang})")

        if not transcript.strip():
            return {"transcript": "", "answer": "Sorry, I could not hear anything clearly. Please try again."}

        # RAG + LLM
        context = await retrieve_context(transcript)
        answer  = await generate_answer(transcript, context, lang)
        log.info(f"[WA-VOICE] Answer: {answer}")

        return {"transcript": transcript, "answer": answer, "language": lang}

    except Exception as e:
        log.error(f"Voice note error: {e}")
        return {"transcript": "", "answer": "Sorry, technical issue processing your voice note."}
    finally:
        os.unlink(tmp_path)
