"""
BSN Voice Chat — Browser-based voice dialog with DeepSeek v4 Flash.

Architecture:
  Browser (Web Speech API + Audio) → Flask proxy → DeepSeek API
  TTS via Piper (switchable DE/EN/ES voices)

Features:
  - Language selector: Deutsch | English | Español
  - DeepSeek v4 Flash as model (direct API, not OpenWebUI)
  - Stop button to interrupt speech
  - Thinking animation while AI processes
  - Light/Dark mode toggle
  - Mobile-responsive layout
  - Design with #20228a primary color
"""

import os
import subprocess
import hashlib
import secrets
import time
import json
from pathlib import Path

from flask import Flask, request, jsonify, send_file, session, Response, stream_with_context
from flask_cors import CORS
from datetime import timedelta

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=24)
CORS(app, supports_credentials=True)

# ── Config ──────────────────────────────────────────────────────────────────
DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"
# Try environment first, then Hermes .env, then fallback
_hermes_env = Path.home() / ".hermes" / ".env"
if not os.environ.get("DEEPSEEK_API_KEY") and _hermes_env.exists():
    with open(_hermes_env) as _f:
        for _line in _f:
            _line = _line.strip()
            if _line.startswith("DEEPSEEK_API_KEY="):
                os.environ["DEEPSEEK_API_KEY"] = _line.split("=", 1)[1]
                break
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
DEEPSEEK_MODEL = "deepseek-v4-flash"
APP_PASSWORD = os.environ.get("APP_PASSWORD", "hermes2026")
PIPER_BIN = "/home/hermes/.local/bin/piper"
PIPER_ENV = {
    "LD_LIBRARY_PATH": "/home/hermes/.local/lib",
    "ESPEAK_DATA_PATH": "/home/hermes/.local/share/espeak-ng-data",
    "PATH": os.environ.get("PATH", "/usr/bin"),
}
TTS_OUTPUT_DIR = Path("/tmp/voice-webapp-tts")
TTS_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# ── Language Configuration ──────────────────────────────────────────────────
LANGUAGES = {
    "de": {
        "label": "Deutsch",
        "flag": "🇩🇪",
        "stt_lang": "de-DE",
        "system_prompt": (
            "Du bist ein hilfreicher Sprachassistent für die Brettspiel-Nachrichten-Website "
            "brettspiel-news.de. Antworte auf Deutsch. Keine Emojis. Kurze, natürliche Sätze "
            "(< 500 Zeichen). Du sprichst via Text-to-Speech, also verwende natürliche "
            "gesprochene Sprache."
        ),
        "piper_model": "/home/hermes/.hermes/cache/piper-voices/de_DE-thorsten-medium.onnx",
    },
    "en": {
        "label": "English",
        "flag": "🇬🇧",
        "stt_lang": "en-US",
        "system_prompt": (
            "You are a helpful voice assistant for a board game news website called "
            "brettspiel-news.de. Answer in English. No emojis. Short natural sentences "
            "(< 500 characters). You are speaking via text-to-speech, so use natural "
            "spoken language."
        ),
        "piper_model": "/home/hermes/.hermes/cache/piper-voices/en_US-joe-medium.onnx",
    },
    "es": {
        "label": "Español",
        "flag": "🇪🇸",
        "stt_lang": "es-ES",
        "system_prompt": (
            "Eres un asistente de voz útil para un sitio web de noticias de juegos de mesa "
            "llamado brettspiel-news.de. Responde en español. Sin emojis. Frases cortas y "
            "naturales (< 500 caracteres). Estás hablando mediante texto a voz, así que "
            "usa lenguaje hablado natural."
        ),
        "piper_model": "/home/hermes/.hermes/cache/piper-voices/es_ES-sharvard-medium.onnx",
    },
}

# ── SPIEL 2025 Hallplan (OPTIONAL — set False to remove) ──────────────────────
USE_HALLPLAN = True
HALLPLAN_FILE = Path(__file__).parent / "data" / "spiel2025_aussteller.json"

# Load hallplan data (empty list if disabled or missing)
hallplan_data = []
if USE_HALLPLAN and HALLPLAN_FILE.exists():
    try:
        with open(HALLPLAN_FILE) as f:
            hallplan_data = json.load(f)
        print(f"[HALLPLAN] Loaded {len(hallplan_data)} exhibitors from SPIEL 2025", flush=True)
    except Exception as e:
        print(f"[HALLPLAN] Error loading: {e}", flush=True)

DEFAULT_LANG = "de"


def get_lang():
    """Return the current session language code (de|en|es)."""
    lang = session.get("language", DEFAULT_LANG)
    if lang not in LANGUAGES:
        lang = DEFAULT_LANG
    return lang


# ── Auth ────────────────────────────────────────────────────────────────────

def check_auth():
    return session.get("authenticated", False)


# ── Routes ──────────────────────────────────────────────────────────────────

@app.route("/")
def index():
    if not check_auth():
        return LOGIN_PAGE
    return CHAT_PAGE


@app.route("/login", methods=["POST"])
def login():
    data = request.get_json(silent=True) or {}
    pw = data.get("password", "")
    if pw == APP_PASSWORD:
        session.permanent = True
        session["authenticated"] = True
        return jsonify({"ok": True})
    time.sleep(0.8)
    return jsonify({"ok": False, "error": "Wrong password"}), 401


@app.route("/logout", methods=["POST"])
def logout():
    session.clear()
    return jsonify({"ok": True})


@app.route("/api/language", methods=["GET", "POST"])
def api_language():
    if not check_auth():
        return jsonify({"error": "Not authenticated"}), 401

    if request.method == "POST":
        data = request.get_json(silent=True) or {}
        lang = data.get("language", DEFAULT_LANG)
        if lang not in LANGUAGES:
            return jsonify({"error": f"Unsupported language: {lang}"}), 400
        session["language"] = lang
        return jsonify({"ok": True, "language": lang})

    # GET — return current language info
    lang = get_lang()
    return jsonify({
        "ok": True,
        "language": lang,
        "languages": {k: {"label": v["label"], "flag": v["flag"], "stt_lang": v["stt_lang"]} for k, v in LANGUAGES.items()},
    })


@app.route("/api/chat", methods=["POST"])
def chat():
    if not check_auth():
        return jsonify({"error": "Not authenticated"}), 401

    data = request.get_json(silent=True) or {}
    user_message = data.get("message", "").strip()
    # Single source of truth: session language
    lang = get_lang()

    if not user_message:
        return jsonify({"error": "No message provided"}), 400

    config = LANGUAGES[lang]

    # Optional: inject hallplan into system prompt for fair-related queries
    system_prompt = config["system_prompt"]

    # Hallplan data always injected when enabled
    if hallplan_data:
        hall_summary = "\n\nSPIEL 2025 Halle 1 — Aussteller und Stände (Referenzdaten):\n"
        for ex in hallplan_data:
            hall_summary += f"- {ex['name']}: Stand {ex['stand']}\n"
        system_prompt += hall_summary + "\nWenn jemand nach einem Aussteller, Stand, Halle, Messe oder SPIEL fragt, nutze diese Liste für die Antwort."

    import urllib.request

    payload = json.dumps({
        "model": DEEPSEEK_MODEL,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message},
        ],
        "max_tokens": 500,
        "temperature": 0.7,
        "stream": False,
    }).encode("utf-8")

    req = urllib.request.Request(
        DEEPSEEK_API_URL,
        data=payload,
        headers={
            "Authorization": f"Bearer {DEEPSEEK_API_KEY}",
            "Content-Type": "application/json",
        },
    )

    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            raw = resp.read().decode("utf-8")
            result = json.loads(raw)
            choices = result.get("choices", [])
            if not choices:
                print(f"[CHAT] No choices in response: {str(result)[:300]}", flush=True)
                return jsonify({"error": "No response from model"}), 502
            msg = choices[0].get("message", {})
            reply = msg.get("content", "")
            if not reply or not reply.strip():
                print(f"[CHAT] Empty content. Finish reason: {choices[0].get('finish_reason')} Full: {str(result)[:300]}", flush=True)
                fallback = {
                    "de": "Entschuldigung, ich konnte keine Antwort generieren. Bitte versuche es erneut.",
                    "en": "Sorry, I could not generate a response. Please try again.",
                    "es": "Lo siento, no pude generar una respuesta. Por favor, inténtalo de nuevo.",
                }
                return jsonify({"ok": True, "reply": fallback.get(lang, fallback["de"])})
            print(f"[CHAT] Reply: {len(reply)} chars [lang={lang}]", flush=True)
            return jsonify({"ok": True, "reply": reply, "language": lang})
    except urllib.error.HTTPError as e:
        err_body = e.read().decode("utf-8", errors="replace")
        return jsonify({"error": f"API error: {err_body[:200]}"}), 502
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/tts", methods=["POST"])
def tts():
    if not check_auth():
        return jsonify({"error": "Not authenticated"}), 401

    data = request.get_json(silent=True) or {}
    text = data.get("text", "").strip()
    # Single source of truth: session language
    lang = get_lang()

    if not text:
        return jsonify({"error": "No text provided"}), 400

    piper_model = LANGUAGES[lang]["piper_model"]

    text_hash = hashlib.md5((lang + ":" + text).encode()).hexdigest()[:12]
    out_path = TTS_OUTPUT_DIR / f"tts_{lang}_{text_hash}.wav"

    # Check cache (cached per language + text)
    if out_path.exists() and out_path.stat().st_size > 1000:
        return send_file(str(out_path), mimetype="audio/wav")

    text = text[:2000]

    try:
        result = subprocess.run(
            [PIPER_BIN, "--model", piper_model, "--output_file", str(out_path)],
            input=text.encode("utf-8"),
            capture_output=True,
            timeout=30,
            env=PIPER_ENV,
        )
        if result.returncode != 0:
            return jsonify({"error": f"TTS failed: {result.stderr.decode()[:200]}"}), 500
        if out_path.exists() and out_path.stat().st_size > 1000:
            return send_file(str(out_path), mimetype="audio/wav")
        return jsonify({"error": "Generated file empty"}), 500
    except subprocess.TimeoutExpired:
        return jsonify({"error": "TTS timeout"}), 500
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/check")
def api_check():
    return jsonify({"authenticated": check_auth()})


# ── Login Page ──────────────────────────────────────────────────────────────

LOGIN_PAGE = """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BSN Voice Chat — Login</title>
<style>
:root { --bg: #0a0a14; --card: #13132a; --text: #e0e0e8; --muted: #8888a0;
        --primary: #20228a; --primary-hover: #2a2cb0; --border: #2a2a44; }
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
       background:var(--bg); color:var(--text); display:flex; justify-content:center;
       align-items:center; min-height:100vh; padding:16px; }
.login-box { background:var(--card); padding:44px 36px; border-radius:16px;
             width:100%; max-width:360px; box-shadow:0 12px 40px rgba(0,0,0,.5);
             border:1px solid var(--border); }
h1 { font-size:26px; margin-bottom:6px; color:#fff; letter-spacing:-0.5px; }
h1 span { color:var(--primary); }
.sub { color:var(--muted); font-size:14px; margin-bottom:28px; line-height:1.5; }
input { width:100%; padding:14px; border:1px solid var(--border);
        background:var(--bg); color:#fff; border-radius:8px; font-size:16px;
        margin-bottom:18px; transition:border-color .2s; }
input:focus { outline:none; border-color:var(--primary); }
button { width:100%; padding:14px; background:var(--primary); color:#fff;
         border:none; border-radius:8px; font-size:16px; font-weight:600;
         cursor:pointer; transition:background .2s; }
button:hover { background:var(--primary-hover); }
.error { color:#ff6b6b; font-size:13px; margin-bottom:14px; display:none;
         text-align:center; }
.brand { text-align:center; margin-top:24px; font-size:11px; color:var(--muted); }
</style>
</head>
<body>
<div class="login-box">
  <h1>🎙 <span>BSN</span> Voice Chat</h1>
  <p class="sub">Voice dialog with AI — speak naturally and get spoken responses.</p>
  <p class="error" id="error">Wrong password</p>
  <input type="password" id="pw" placeholder="Password" autofocus>
  <button onclick="doLogin()">Log in</button>
  <div class="brand">brettspiel-news.de</div>
</div>
<script>
document.getElementById('pw').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });
async function doLogin() {
  const r = await fetch('/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:document.getElementById('pw').value})});
  if(r.ok) window.location.reload();
  else document.getElementById('error').style.display='block';
}
</script>
</body>
</html>"""


# ── Chat Page ───────────────────────────────────────────────────────────────

CHAT_PAGE = """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>BSN Voice Chat</title>
<style>
/* ── Theme Variables ── */
:root, [data-theme="dark"] {
  --bg: #0a0a14; --surface: #111128; --card: #18183a; --card-agent: #151538;
  --text: #e0e0e8; --text-dim: #a0a0b8; --muted: #707090;
  --primary: #20228a; --primary-bright: #2e30b8; --primary-glow: rgba(32,34,138,.35);
  --user-msg: #20228a; --agent-msg: #18183a; --agent-border: #2a2a50;
  --input-bg: #0d0d20; --input-border: #2a2a50;
  --header-bg: #0d0d22; --controls-bg: #0d0d22;
  --stop-btn: #e74c3c; --shadow: rgba(0,0,0,.5);
}
[data-theme="light"] {
  --bg: #f4f4f8; --surface: #ffffff; --card: #fafafe; --card-agent: #f0f0f8;
  --text: #1a1a2e; --text-dim: #555570; --muted: #8888a0;
  --primary: #20228a; --primary-bright: #2e30b8; --primary-glow: rgba(32,34,138,.15);
  --user-msg: #20228a; --agent-msg: #f5f5fc; --agent-border: #d0d0e0;
  --input-bg: #ffffff; --input-border: #d0d0e0;
  --header-bg: #ffffff; --controls-bg: #ffffff;
  --stop-btn: #e74c3c; --shadow: rgba(0,0,0,.1);
}

* { margin:0; padding:0; box-sizing:border-box; }
html, body { height:100%; overflow:hidden; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
       background:var(--bg); color:var(--text); display:flex; flex-direction:column;
       transition:background .3s,color .3s; }

/* ── Header ── */
.header { background:var(--header-bg); padding:8px 12px; display:flex;
          flex-wrap:wrap; align-items:center; gap:6px;
          border-bottom:1px solid var(--input-border); }
.header-left { display:flex; align-items:center; gap:8px; flex-shrink:0; }
.header h1 { font-size:15px; color:var(--text); white-space:nowrap; }
.header h1 span { color:var(--primary); font-weight:800; }

/* ── Language Dropdown ── */
.lang-select { background:var(--card); color:var(--text); border:1px solid var(--input-border);
               border-radius:6px; padding:4px 6px; font-size:12px; cursor:pointer;
               transition:border-color .2s; outline:none; }
.lang-select:hover, .lang-select:focus { border-color:var(--primary); }

/* ── Theme Toggle Switch ── */
.switch-wrap { display:flex; align-items:center; gap:4px; }
.switch-track { width:36px; height:20px; background:var(--card); border:1px solid var(--input-border);
                border-radius:10px; cursor:pointer; position:relative; transition:all .3s; flex-shrink:0; }
.switch-track.active { background:var(--primary); border-color:var(--primary); }
.switch-knob { width:14px; height:14px; background:var(--text-dim); border-radius:50%;
               position:absolute; top:2px; left:2px; transition:all .3s; }
.switch-track.active .switch-knob { background:#fff; left:18px; }
.switch-icon { font-size:12px; line-height:1; user-select:none; }
#status { font-size:10px; padding:2px 8px; border-radius:8px;
          background:var(--card); color:var(--muted); font-weight:500; white-space:nowrap; }
#status.listening { background:var(--primary); color:#fff; animation:pulse 1.2s infinite; }
#status.thinking { background:#f39c12; color:#fff; }
#status.speaking { background:#27ae60; color:#fff; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
.header-right { display:flex; gap:4px; align-items:center; flex-wrap:wrap; margin-left:auto; }

/* ── Messages ── */
.messages { flex:1; overflow-y:auto; padding:12px; display:flex;
            flex-direction:column; gap:10px; -webkit-overflow-scrolling:touch;
            min-height:0; }
.msg { max-width:85%; padding:10px 14px; border-radius:12px; line-height:1.5;
       position:relative; word-wrap:break-word; font-size:15px; }
.msg.user { align-self:flex-end; background:var(--user-msg); color:#fff;
            border-bottom-right-radius:4px; }
.msg.agent { align-self:flex-start; background:var(--agent-msg); color:var(--text);
             border:1px solid var(--agent-border); border-bottom-left-radius:4px; }
.msg .time { font-size:9px; color:var(--muted); margin-top:4px; }
.interim { align-self:flex-end; color:var(--muted); font-style:italic;
           padding:0 14px; max-width:85%; font-size:13px; }

/* ── Thinking Overlay ── */
.thinking-overlay {
  display:none; position:fixed; top:0; left:0; right:0; bottom:0;
  z-index:1000; background:rgba(255,255,255,0.75);
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  align-items:center; justify-content:center;
  flex-direction:column; gap:36px;
}
.thinking-overlay.show { display:flex; }
[data-theme="light"] .thinking-overlay {
  background:rgba(255,255,255,0.82);
}

/* Orb — rotating gradient ring */
.orb-ring {
  position:relative; width:140px; height:140px;
  display:flex; align-items:center; justify-content:center;
}
.orb-ring svg {
  width:140px; height:140px; animation:orb-spin 1.8s cubic-bezier(0.4,0.0,0.2,1) infinite;
}
.orb-ring circle {
  fill:none; stroke-width:4; stroke-linecap:round;
  stroke:url(#orbGrad);
  stroke-dasharray:240; stroke-dashoffset:170;
  animation:orb-dash 2.4s ease-in-out infinite;
}
@keyframes orb-spin {
  100% { transform:rotate(360deg); }
}
@keyframes orb-dash {
  0% { stroke-dashoffset:260; }
  50% { stroke-dashoffset:60; }
  100% { stroke-dashoffset:260; }
}

/* Inner pulsing glow */
.orb-glow {
  position:absolute; width:70px; height:70px;
  border-radius:50%;
  background:radial-gradient(circle, rgba(32,34,138,0.4) 0%, rgba(32,34,138,0.1) 50%, transparent 70%);
  animation:orb-pulse 2s ease-in-out infinite;
}
@keyframes orb-pulse {
  0%,100% { transform:scale(0.7); opacity:0.4; }
  50% { transform:scale(1.3); opacity:1; }
}

/* Floating particles */
.particles { position:absolute; width:140%; height:140%; pointer-events:none; }
.particle {
  position:absolute; width:5px; height:5px;
  border-radius:50%;
  background:#5058d0;
  opacity:0;
  animation:particle-float 3.5s ease-in-out infinite;
}
.particle:nth-child(1) { left:10%; top:25%; animation-delay:0s; }
.particle:nth-child(2) { left:80%; top:15%; animation-delay:0.6s; width:4px; height:4px; }
.particle:nth-child(3) { left:20%; top:75%; animation-delay:1.2s; width:6px; height:6px; }
.particle:nth-child(4) { left:85%; top:70%; animation-delay:0.3s; }
.particle:nth-child(5) { left:50%; top:5%; animation-delay:1.5s; width:4px; height:4px; }
.particle:nth-child(6) { left:5%; top:50%; animation-delay:0.9s; }
@keyframes particle-float {
  0% { transform:translateY(10px) scale(0.5); opacity:0; }
  30% { opacity:0.9; }
  70% { opacity:0.5; }
  100% { transform:translateY(-60px) scale(1.2); opacity:0; }
}

/* Thinking label */
.thinking-label {
  font-size:15px; color:#a0a8d0; letter-spacing:3px;
  text-transform:uppercase; font-weight:600;
  animation:label-pulse 2s ease-in-out infinite;
}
@keyframes label-pulse {
  0%,100% { opacity:0.4; }
  50% { opacity:1; }
}

/* ── Controls ── */
.controls { background:var(--controls-bg); padding:8px 12px; display:flex;
            gap:8px; align-items:center; border-top:1px solid var(--input-border);
            padding-bottom:env(safe-area-inset-bottom,8px); }
.mic-btn { width:44px; height:44px; border-radius:50%; border:2px solid var(--input-border);
           background:var(--card); color:var(--text-dim); font-size:18px;
           cursor:pointer; display:flex; align-items:center; justify-content:center;
           transition:all .2s; flex-shrink:0; }
.mic-btn:hover { border-color:var(--primary); color:var(--primary-bright); }
.mic-btn.active { border-color:var(--stop-btn); background:var(--stop-btn);
                  color:#fff; box-shadow:0 0 14px rgba(231,76,60,.4);
                  animation:micPulse 1.5s infinite; }
@keyframes micPulse { 0%,100%{box-shadow:0 0 6px rgba(231,76,60,.3)}
                      50%{box-shadow:0 0 18px rgba(231,76,60,.5)} }
.text-input { flex:1; min-width:60px; padding:11px 14px; background:var(--input-bg);
              border:1px solid var(--input-border); color:var(--text);
              border-radius:8px; font-size:15px; }
.text-input:focus { outline:none; border-color:var(--primary); }
.text-input::placeholder { color:var(--muted); font-size:13px; }
.send-btn { padding:10px 16px; background:var(--primary); color:#fff;
            border:none; border-radius:8px; cursor:pointer; font-size:14px;
            font-weight:600; transition:background .2s; white-space:nowrap; }
.send-btn:hover { background:var(--primary-bright); }
.send-btn:disabled { background:var(--muted); cursor:not-allowed; opacity:.6; }

/* ── Stop Button ── */
.stop-btn { display:none; padding:10px 16px; background:var(--stop-btn); color:#fff;
            border:none; border-radius:8px; cursor:pointer; font-size:14px;
            font-weight:600; animation:pulse .8s infinite; white-space:nowrap; }
.stop-btn.show { display:block; }
.logout-btn { background:none; border:1px solid var(--input-border); color:var(--muted);
              padding:3px 8px; border-radius:5px; cursor:pointer; font-size:11px;
              line-height:1.2; }
.logout-btn:hover { border-color:var(--text-dim); color:var(--text); }

/* ── Placeholder ── */
.placeholder { flex:1; display:flex; flex-direction:column; align-items:center;
               justify-content:center; color:var(--muted); font-size:14px;
               gap:10px; user-select:none; padding:20px; }
.placeholder-big { font-size:36px; }

/* ── Mobile overrides ── */
@media (max-width:480px) {
  .header { padding:6px 10px; gap:4px; }
  .header h1 { font-size:13px; }
  .lang-select { font-size:11px; padding:3px 5px; }
  .switch-track { width:32px; height:18px; }
  .switch-knob { width:12px; height:12px; }
  .switch-track.active .switch-knob { left:16px; }
  .header-right { gap:3px; }
  .controls { padding:6px 10px; gap:6px; }
  .mic-btn { width:40px; height:40px; font-size:16px; }
  .text-input { font-size:14px; padding:9px 12px; }
  .send-btn { padding:8px 14px; font-size:13px; }
  .msg { max-width:90%; font-size:14px; }
  .orb-ring { width:100px; height:100px; }
  .orb-ring svg { width:100px; height:100px; }
  .orb-ring circle { r:34; stroke-width:3.5; }
  .orb-glow { width:50px; height:50px; }
  .particle { width:4px; height:4px; }
  .thinking-label { font-size:13px; }
}
</style>
</head>
<body>

<div class="header">
  <div class="header-left">
    <h1>🎙 <span>BSN</span> Chat</h1>
    <span id="status">Ready</span>
  </div>
  <div class="header-right">
    <select class="lang-select" id="langSelect" onchange="switchLang(this.value)">
      <option value="de">🇩🇪 Deutsch</option>
      <option value="en">🇬🇧 English</option>
      <option value="es">🇪🇸 Español</option>
    </select>
    <div class="switch-wrap">
      <span class="switch-icon">☀️</span>
      <div class="switch-track" id="themeSwitch" onclick="toggleTheme()">
        <div class="switch-knob"></div>
      </div>
      <span class="switch-icon">🌙</span>
    </div>
    <button class="logout-btn" onclick="logout()">Logout</button>
  </div>
</div>

<div class="messages" id="messages">
  <div class="placeholder" id="placeholder">
    <div class="placeholder-big">🎙</div>
    <div>Start speaking or type a message</div>
  </div>
</div>

<!-- Thinking Overlay -->
<div class="thinking-overlay" id="thinkingOverlay">
  <svg width="0" height="0" style="position:absolute">
    <defs>
      <linearGradient id="orbGrad" x1="0%" y1="0%" x2="100%" y2="100%">
        <stop offset="0%" stop-color="#20228a"/>
        <stop offset="40%" stop-color="#3a3db0"/>
        <stop offset="100%" stop-color="#585cd0"/>
      </linearGradient>
    </defs>
  </svg>
  <div class="orb-ring">
    <svg viewBox="0 0 80 80">
      <circle cx="40" cy="40" r="36"/>
    </svg>
    <div class="particles">
      <div class="particle"></div>
      <div class="particle"></div>
      <div class="particle"></div>
      <div class="particle"></div>
      <div class="particle"></div>
      <div class="particle"></div>
    </div>
    <div class="orb-glow"></div>
  </div>
  <div class="thinking-label">Thinking</div>
</div>

<div id="interim" class="interim" style="display:none"></div>

<div class="controls">
  <button class="mic-btn" id="micBtn" onclick="toggleMic()" title="Click to talk">🎤</button>
  <input type="text" class="text-input" id="textInput" placeholder="Nachricht" autofocus>
  <button class="send-btn" id="sendBtn" onclick="sendText()">Send</button>
  <button class="stop-btn" id="stopBtn" onclick="stopAll()">⏹</button>
</div>

<audio id="player" preload="auto"></audio>

<script>
// ── State ──
let listening = false;
let recognition = null;
let abortController = null;
let currentLang = 'de';
let langNames = {de:'Deutsch', en:'English', es:'Español'};
let sttLangs = {de:'de-DE', en:'en-US', es:'es-ES'};

// ── Theme ──
function toggleTheme() {
  const html = document.documentElement;
  const current = html.getAttribute('data-theme');
  const next = current === 'light' ? 'dark' : 'light';
  html.setAttribute('data-theme', next);
  document.getElementById('themeSwitch').classList.toggle('active', next === 'dark');
  localStorage.setItem('theme', next);
}

(function initTheme() {
  const saved = localStorage.getItem('theme') || 'dark';
  document.documentElement.setAttribute('data-theme', saved);
  document.getElementById('themeSwitch').classList.toggle('active', saved === 'dark');
})();

// ── Language ──
async function loadLang() {
  try {
    const r = await fetch('/api/language');
    const data = await r.json();
    if (data.ok) {
      currentLang = data.language;
      // Store STT langs from server
      sttLangs = {};
      for (const [k, v] of Object.entries(data.languages)) {
        sttLangs[k] = v.stt_lang;
      }
      updateLangUI(currentLang);
    }
  } catch(e) {}
}

function updateLangUI(lang) {
  currentLang = lang;
  document.getElementById('langSelect').value = lang;
  // Update STT language
  if (recognition) {
    recognition.lang = sttLangs[lang] || (lang === 'de' ? 'de-DE' : lang === 'es' ? 'es-ES' : 'en-US');
  }
  const input = document.getElementById('textInput');
  const placeholders = {de:'Nachricht', en:'Message', es:'Mensaje'};
  if (input) input.placeholder = placeholders[lang] || placeholders['de'];
}

async function switchLang(lang) {
  try {
    const r = await fetch('/api/language', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({language: lang}),
    });
    const data = await r.json();
    if (data.ok) {
      updateLangUI(lang);
      const msgs = {de:'🇩🇪 Deutsch', en:'🇬🇧 English', es:'🇪🇸 Español'};
      addMessage('agent', msgs[lang]);
    }
  } catch(e) {}
}

// ── Web Speech API Init ──
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
  const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
  recognition = new SR();
  recognition.lang = 'de-DE';
  recognition.interimResults = true;
  recognition.continuous = false;

  recognition.onresult = (e) => {
    let interim = '', final = '';
    for (let i = e.resultIndex; i < e.results.length; i++) {
      if (e.results[i].isFinal) final += e.results[i][0].transcript;
      else interim += e.results[i][0].transcript;
    }
    const interEl = document.getElementById('interim');
    if (interim) {
      interEl.style.display = 'block';
      interEl.textContent = interim;
    } else {
      interEl.style.display = 'none';
    }
    if (final) {
      interEl.style.display = 'none';
      stopListening();
      sendMessage(final.trim());
    }
  };

  recognition.onerror = (e) => {
    console.error('Speech error:', e.error);
    stopListening();
    setStatus('Ready');
  };

  recognition.onend = () => {
    if (listening) {
      try { recognition.start(); } catch(_) {}
    } else {
      setStatus('Ready');
    }
  };
} else {
  document.getElementById('micBtn').style.display = 'none';
}

function toggleMic() {
  if (!recognition) return;
  listening ? stopListening() : startListening();
}

function startListening() {
  listening = true;
  const btn = document.getElementById('micBtn');
  btn.classList.add('active');
  btn.textContent = '⬤';
  setStatus('Listening...', 'listening');
  try { recognition.start(); } catch(_) {}
}

function stopListening() {
  listening = false;
  const btn = document.getElementById('micBtn');
  btn.classList.remove('active');
  btn.textContent = '🎤';
}

function setStatus(text, cls) {
  const el = document.getElementById('status');
  el.textContent = text;
  el.className = cls || '';
}

function showThinking(show) {
  document.getElementById('thinkingOverlay').classList.toggle('show', show);
}

function showStop(show) {
  document.getElementById('stopBtn').classList.toggle('show', show);
  if (show) {
    document.getElementById('sendBtn').style.display = 'none';
  } else {
    document.getElementById('sendBtn').style.display = '';
  }
}

function addMessage(role, text) {
  const placeholder = document.getElementById('placeholder');
  if (placeholder) placeholder.style.display = 'none';
  const div = document.createElement('div');
  div.className = 'msg ' + role;
  const now = new Date();
  const time = now.getHours().toString().padStart(2,'0') + ':' +
               now.getMinutes().toString().padStart(2,'0');
  div.innerHTML = text + '<div class="time">' + time + '</div>';
  const msgs = document.getElementById('messages');
  msgs.appendChild(div);
  msgs.scrollTop = msgs.scrollHeight;
}

// ── Stop All ──
function stopAll() {
  if (abortController) {
    abortController.abort();
    abortController = null;
  }
  const player = document.getElementById('player');
  if (player) { player.pause(); player.src = ''; }
  showThinking(false);
  showStop(false);
  setStatus('Ready');
  document.getElementById('sendBtn').disabled = false;
}

// ── Send Message ──
async function sendMessage(text) {
  if (!text) return;
  addMessage('user', text);
  showThinking(true);
  showStop(true);
  setStatus('Thinking...', 'thinking');

  const sendBtn = document.getElementById('sendBtn');
  sendBtn.disabled = true;

  abortController = new AbortController();

  try {
    const r = await fetch('/api/chat', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({ message: text }),
      signal: abortController.signal,
    });

    showThinking(false);

    if (!r.ok) {
      const err = await r.json().catch(() => ({}));
      addMessage('agent', '❌ Error: ' + (err.error || 'Request failed'));
      return;
    }

    const data = await r.json();
    if (data.ok && data.reply) {
      // Sync language from server response
      if (data.language) {
        currentLang = data.language;
        updateLangUI(data.language);
      }
      addMessage('agent', data.reply);
      setStatus('Speaking...', 'speaking');
      await playTTS(data.reply);
    } else {
      addMessage('agent', '❌ No reply received');
    }
  } catch(e) {
    if (e.name === 'AbortError') {
      addMessage('agent', '⏹ Stopped');
    } else {
      addMessage('agent', '❌ Connection error');
    }
    showThinking(false);
  } finally {
    setStatus('Ready');
    sendBtn.disabled = false;
    showStop(false);
    if (abortController) abortController = null;
  }
}

function sendText() {
  const input = document.getElementById('textInput');
  const text = input.value.trim();
  if (!text) return;
  input.value = '';
  sendMessage(text);
}

document.getElementById('textInput').addEventListener('keydown', e => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    sendText();
  }
});

// ── TTS ──
async function playTTS(text) {
  try {
    const r = await fetch('/api/tts', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({ text }),
      signal: abortController ? abortController.signal : undefined,
    });
    if (!r.ok) { console.error('TTS error:', r.status); return; }
    const blob = await r.blob();
    const url = URL.createObjectURL(blob);
    const player = document.getElementById('player');
    player.src = url;
    showStop(true);
    await player.play();
    await new Promise((resolve) => {
      player.onended = resolve;
      player.onerror = resolve;
      player.onabort = resolve;

      const checkAbort = setInterval(() => {
        if (abortController && abortController.signal.aborted) {
          player.pause();
          player.src = '';
          resolve();
        }
      }, 200);

      player.addEventListener('ended', () => clearInterval(checkAbort));
      player.addEventListener('error', () => clearInterval(checkAbort));
    });
    URL.revokeObjectURL(url);
  } catch(e) {}
}

// ── Logout ──
async function logout() {
  await fetch('/logout', { method:'POST' });
  window.location.reload();
}

// ── Init ──
// iOS Safari height fix: set body height on resize
function fixHeight() {
  document.body.style.height = window.innerHeight + 'px';
}
window.addEventListener('resize', fixHeight);
window.addEventListener('orientationchange', function() { setTimeout(fixHeight, 200); });

loadLang();
fetch('/api/check').then(r => r.json()).then(d => {
  if (!d.authenticated) window.location.reload();
});
fixHeight();
</script>
</body>
</html>"""


# ── Main ────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("Starting BSN Voice Chat on port 5001 (DeepSeek v4 Flash)...")
    app.run(host="::", port=5001, debug=False)