"use client";

import { createPrompt, deletePrompt, updatePrompt } from "@/lib/features/chatbot/prompt/action";
import {
  ActionIcon, Alert, Badge, Button, Divider, Group, NumberInput, Paper, Select, Slider, Stack,
  Switch, Table, Text, Textarea, TextInput, ThemeIcon, Tooltip,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import {
  IconCheck, IconInfoCircle, IconMicrophone, IconPlayerPlay,
  IconPlus, IconRefresh, IconRocket, IconTrash, IconVolume, IconEdit,
} from "@tabler/icons-react";
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router";

// ── Types ─────────────────────────────────────────────────────────────────────

export type VoicePromptEntry = {
  id:           string;
  prompt_name:  string;
  prompt_key:   string;
  prompt_value: string;
  created_at?:  string;
  updated_at?:  string;
};

export type VoiceTextPrompt = {
  id:          string;
  prompt_name: string;
  prompt_key:  string;
  prompt_value: string;
  created_by:  string;
  updated_by:  string;
};

export type VoiceRecordingConfig = {
  minRecordMs:        number;
  silenceDuration:    number;
  maxRecordMs:        number;
  calibrationMs:      number;
  noiseMarginDefault: number;
  noiseMarginHigh:    number;
  bargeInFrames:      number;
  levelFps:           number;
  bargeInNoiseMargin?:   number;
  maxSpokenSentences?:   number;
  maxSpokenChars?:       number;
  disableCrawl?:         boolean;
  voiceLanguageModel?:   string;
};

export type VoiceCommandGroup = {
  command:  string;    // internal name, e.g. "start_fresh"
  phrases:  string[];  // spoken phrases that trigger this command
  response: string;    // what the agent says back
};

export type VoiceConfig = {
  ttsConfigId:              string | null;
  charPromptId:             string | null;
  recordingConfigId:        string | null;
  activeTextPromptConfigId: string | null;
  voiceCommandsConfigId:    string | null;
  voiceCommandsData:        { commands: VoiceCommandGroup[] };
  activeTextPromptIds:      string[];
  ttsConfig:                { rate: number; pitch: number; volume: number; voiceURI: string; voiceName: string; genderHint: string; ignoreBackgroundNoise: boolean; allowInterrupt: boolean; serverVoice?: string };
  recordingConfig:          VoiceRecordingConfig;
  activeVoiceName:          string | null;
  activeCharPrompt:         string | null;
  experiments:              VoicePromptEntry[];
  voiceTextPrompts:         VoiceTextPrompt[];
  productionRecords?: {
    tts?:       { id: string; name: string; key: string } | null;
    char?:      { id: string; name: string; key: string } | null;
    recording?: { id: string; name: string; key: string } | null;
    active?:    { id: string; name: string; key: string } | null;
  };
};

type VoicePromptData = {
  characterPrompt: string;
  ttsConfig: {
    rate: number; pitch: number; volume: number;
    voiceURI: string; voiceName: string; lang: string; genderHint: "male";
  };
  characterName: string;
};

// ── Kokoro voices grouped by accent ──────────────────────────────────────────
const KOKORO_VOICES: Record<string, { value: string; label: string }[]> = {
  "en-GB": [
    { value: "bm_george",   label: "George (British Male)"    },
    { value: "bm_lewis",    label: "Lewis (British Male)"     },
    { value: "bm_daniel",   label: "Daniel (British Male)"    },
    { value: "bm_fable",    label: "Fable (British Male)"     },
    { value: "bf_emma",     label: "Emma (British Female)"    },
    { value: "bf_isabella", label: "Isabella (British Female)" },
    { value: "bf_alice",    label: "Alice (British Female)"   },
    { value: "bf_lily",     label: "Lily (British Female)"    },
  ],
  "en-US": [
    { value: "am_adam",    label: "Adam (American Male)"    },
    { value: "am_michael", label: "Michael (American Male)" },
    { value: "am_echo",    label: "Echo (American Male)"    },
    { value: "am_eric",    label: "Eric (American Male)"    },
    { value: "am_liam",    label: "Liam (American Male)"    },
    { value: "am_onyx",    label: "Onyx (American Male)"    },
    { value: "af_heart",   label: "Heart (American Female)" },
    { value: "af_alloy",   label: "Alloy (American Female)" },
    { value: "af_bella",   label: "Bella (American Female)" },
    { value: "af_nicole",  label: "Nicole (American Female)"},
    { value: "af_nova",    label: "Nova (American Female)"  },
    { value: "af_sarah",   label: "Sarah (American Female)" },
    { value: "af_sky",     label: "Sky (American Female)"   },
  ],
};

// ── Accent options (Kokoro supports British & American only) ──────────────────
const ACCENT_LANG_HINTS = [
  { value: "en-GB", label: "British English"  },
  { value: "en-US", label: "American English" },
];

// ── TTS helpers ───────────────────────────────────────────────────────────────
const TTS_HEADER = "## TTS Voice Settings";

function buildTtsBlock(rate: number, pitch: number, volume: number): string {
  return [TTS_HEADER, `rate: ${rate.toFixed(1)}`, `pitch: ${pitch.toFixed(1)}`, `volume: ${volume.toFixed(1)}`].join("\n");
}

function parseTtsFromText(text: string): { rate: number; pitch: number; volume: number } | null {
  const idx = text.indexOf(TTS_HEADER);
  if (idx === -1) return null;
  const section = text.slice(idx);
  const get = (key: string): number | null => {
    const m = section.match(new RegExp(`^${key}:\\s*([\\d.]+)`, "m"));
    return m ? parseFloat(m[1]) : null;
  };
  const rate = get("rate"); const pitch = get("pitch"); const volume = get("volume");
  if (rate === null && pitch === null && volume === null) return null;
  return { rate: rate ?? 1, pitch: pitch ?? 1, volume: volume ?? 1 };
}

function injectTtsBlock(text: string, rate: number, pitch: number, volume: number): string {
  const block = buildTtsBlock(rate, pitch, volume);
  const idx = text.indexOf(TTS_HEADER);
  if (idx === -1) return text.trimEnd() + "\n\n" + block;
  const before = text.slice(0, idx);
  const after  = text.slice(idx).split("\n").slice(4).join("\n");
  return (before + block + (after.startsWith("\n") ? after : "\n" + after)).trimEnd();
}

function assemblePrompt(characterName: string, personality: string, voiceInstructions: string, rate: number, pitch: number, volume: number): string {
  const name   = characterName.trim() || "[Character Name]";
  const traits = personality.trim()   || "[personality traits]";
  const voice  = voiceInstructions.trim();
  return [
    `You are ${name}, ${traits}.`,
    "",
    "## Identity & Persona",
    `- Your name is ${name}. Always refer to yourself as ${name}.`,
    "- Stay in character at all times. Never break persona.",
    "- Maintain your personality consistently whether the conversation is short or long.",
    `- You represent Karma Group — speak with genuine pride and warmth about the brand.`,
    "",
    "## Voice & Speaking Style",
    "- Speak naturally and conversationally — as if talking out loud, not writing a report.",
    "- Use short sentences. One idea per sentence. Avoid walls of text.",
    "- Mirror the user's energy: match their pace, formality, and mood.",
    "- Never open with hollow fillers like 'Certainly!', 'Of course!', 'Absolutely!' — start with the actual answer.",
    "- Use contractions naturally: 'I'll', 'you're', 'that's', 'it's', 'we've'.",
    ...(voice ? ["", "## Accent & Character Voice", voice] : []),
    "",
    "## Tone",
    "- Warm, approachable, and confident.",
    "- Empathetic — acknowledge the user's situation before jumping to answers.",
    "- Positive but honest — never over-promise or use hollow affirmations.",
    "",
    "## Greetings & Introductions",
    `- When greeting a user for the first time, introduce yourself briefly as ${name}.`,
    "- Keep greetings short and natural. Do not recite a scripted welcome speech.",
    "",
    "## Areas of Expertise",
    "You are knowledgeable across all Karma Group offerings. Identify what the user needs and respond accordingly:",
    "",
    "**Resorts & Destinations** — Recommend resorts based on travel style, destination, group size, and dates. Paint a picture of the experience, not just features.",
    "**Membership & Karma Club** — Explain tier benefits, points, member rates, and lifestyle privileges clearly and specifically.",
    "**Bookings & Reservations** — Help with room types, availability, rates, and modifications. Always confirm key details (dates, guests, preferences) before suggesting options.",
    "**Wellness, Spa & Dining** — Speak with warmth and specificity. Describe the experience as though the user is already there.",
    "**Concierge & Activities** — Anticipate needs, offer to arrange transfers, tours, and special celebrations. Make it feel effortless.",
    "",
    "## Conversation Behaviour",
    "- Ask one clarifying question at a time — never overwhelm with multiple questions at once.",
    "- When switching topics, acknowledge the shift naturally and stay helpful.",
    `- If a question is outside your knowledge, say so honestly: \"That's one for our team — let me make sure the right person follows up with you.\"`,
    "- End responses with a natural follow-up or open invitation to continue, not a forced sign-off.",
    "",
    buildTtsBlock(rate, pitch, volume),
  ].join("\n");
}

// ── Helpers ───────────────────────────────────────────────────────────────────
function parseVoicePrompt(row: VoicePromptEntry): VoicePromptData | null {
  try { return JSON.parse(row.prompt_value) as VoicePromptData; } catch { return null; }
}

// ── Slider marks ──────────────────────────────────────────────────────────────
const RATE_MARKS   = [{ value: 0.5, label: "0.5×" }, { value: 1, label: "Normal" }, { value: 1.5, label: "1.5×" }, { value: 2, label: "2×" }];
const PITCH_MARKS  = [{ value: 0.5, label: "Low" },  { value: 1, label: "Normal" }, { value: 1.5, label: "High" }, { value: 2, label: "2.0" }];
const VOLUME_MARKS = [{ value: 0.1, label: "Quiet" }, { value: 0.5, label: "Mid" }, { value: 1, label: "Full" }];

// ── Component ─────────────────────────────────────────────────────────────────
export default function VoiceTab({ voiceConfig }: { voiceConfig: VoiceConfig }) {
  const navigate = useNavigate();
  const [loading,       setLoading]       = useState(false);
  const [error,         setError]         = useState<string | null>(null);
  const [savedName,     setSavedName]     = useState<string | null>(null);
  const [promptText,    setPromptText]    = useState("");
  const manuallyEdited = useRef(false);

  // Local copy of experiments so we can add/remove without page reload
  const [voicePrompts,     setVoicePrompts]     = useState<VoicePromptEntry[]>(voiceConfig.experiments ?? []);
  const [voiceTextPrompts, setVoiceTextPrompts] = useState<VoiceTextPrompt[]>(voiceConfig.voiceTextPrompts ?? []);
  const [deletingTextId,   setDeletingTextId]   = useState<string | null>(null);
  const [togglingTextId,   setTogglingTextId]   = useState<string | null>(null);

  // Inline editing for voice testing prompts
  const [expandedTextId,   setExpandedTextId]   = useState<string | null>(null);
  const [editingTextName,  setEditingTextName]  = useState("");
  const [editingTextValue, setEditingTextValue] = useState("");
  const [savingTextEditId, setSavingTextEditId] = useState<string | null>(null);

  // Multiple voice testing prompts can be active simultaneously
  const [activeTextPromptIds,      setActiveTextPromptIds]      = useState<Set<string>>(new Set(voiceConfig.activeTextPromptIds ?? []));
  const [activeTextPromptConfigId, setActiveTextPromptConfigId] = useState<string | null>(voiceConfig.activeTextPromptConfigId);

  // Track which experiment key is currently in production.
  // On mount: find the experiment whose prompt_name matches the stored voice_character_prompt name.
  // On "Use in Production": update locally so the badge changes immediately.
  const [productionExpKey, setProductionExpKey] = useState<string | null>(() => {
    if (!voiceConfig.activeCharPrompt) return null;
    const match = (voiceConfig.experiments ?? []).find(
      (e) => e.prompt_name === voiceConfig.activeCharPrompt,
    );
    return match?.prompt_key ?? null;
  });
  const [localActiveVoice, setLocalActiveVoice] = useState<string | null>(voiceConfig.activeVoiceName);
  const [localActiveChar,  setLocalActiveChar]  = useState<string | null>(voiceConfig.activeCharPrompt);
  const [justUpdated,      setJustUpdated]      = useState(false);

  const markJustUpdated = () => {
    setJustUpdated(true);
    setTimeout(() => setJustUpdated(false), 5000);
  };

  // Per-row loading states
  const [promotingId, setPromotingId] = useState<string | null>(null);
  const [deletingId,  setDeletingId]  = useState<string | null>(null);

  // Inline editing for voice prompt drafts
  const [expandedId,   setExpandedId]   = useState<string | null>(null);
  const [editingText,  setEditingText]  = useState("");
  const [editingName,  setEditingName]  = useState("");
  const [savingEditId, setSavingEditId] = useState<string | null>(null);

  // Local copy of production records so we can remove entries after delete
  const [prodRecords, setProdRecords] = useState(() => ({
    tts:       voiceConfig.productionRecords?.tts       ?? null,
    char:      voiceConfig.productionRecords?.char      ?? null,
    recording: voiceConfig.productionRecords?.recording ?? null,
    active:    voiceConfig.productionRecords?.active    ?? null,
  }));
  const [deletingProdKey, setDeletingProdKey] = useState<string | null>(null);

  // Mutable IDs for production records (may be created fresh via "Use in Production")
  const [ttsConfigId,        setTtsConfigId]        = useState<string | null>(voiceConfig.ttsConfigId);
  const [charPromptId,       setCharPromptId]       = useState<string | null>(voiceConfig.charPromptId);
  const [recordingConfigId,  setRecordingConfigId]  = useState<string | null>(voiceConfig.recordingConfigId);

  // Recording behaviour constants — all controlled from admin console
  const [minRecordMs,        setMinRecordMs]        = useState(voiceConfig.recordingConfig.minRecordMs);
  const [silenceDuration,    setSilenceDuration]    = useState(voiceConfig.recordingConfig.silenceDuration);
  const [maxRecordMs,        setMaxRecordMs]        = useState(voiceConfig.recordingConfig.maxRecordMs);
  const [calibrationMs,      setCalibrationMs]      = useState(voiceConfig.recordingConfig.calibrationMs);
  const [noiseMarginDefault, setNoiseMarginDefault] = useState(voiceConfig.recordingConfig.noiseMarginDefault);
  const [noiseMarginHigh,    setNoiseMarginHigh]    = useState(voiceConfig.recordingConfig.noiseMarginHigh);
  const [bargeInFrames,      setBargeInFrames]      = useState(voiceConfig.recordingConfig.bargeInFrames);
  const [levelFps,           setLevelFps]           = useState(voiceConfig.recordingConfig.levelFps);
  const [bargeInNoiseMargin,   setBargeInNoiseMargin]   = useState(voiceConfig.recordingConfig.bargeInNoiseMargin   ?? 10);
  const [maxSpokenSentences,   setMaxSpokenSentences]   = useState(voiceConfig.recordingConfig.maxSpokenSentences   ?? 2);
  const [maxSpokenChars,       setMaxSpokenChars]       = useState(voiceConfig.recordingConfig.maxSpokenChars       ?? 250);
  const [disableCrawl,         setDisableCrawl]         = useState(voiceConfig.recordingConfig.disableCrawl         ?? true);
  const [voiceLanguageModel,   setVoiceLanguageModel]   = useState(voiceConfig.recordingConfig.voiceLanguageModel   ?? "gemini-2.0-flash-lite");
  const [savingRecording,      setSavingRecording]      = useState(false);
  const [savedRecording,       setSavedRecording]       = useState(false);


  const [rate,   setRate]   = useState(voiceConfig.ttsConfig.rate);
  const [pitch,  setPitch]  = useState(voiceConfig.ttsConfig.pitch);
  const [volume, setVolume] = useState(voiceConfig.ttsConfig.volume);
  const [ignoreBackgroundNoise, setIgnoreBackgroundNoise] = useState(voiceConfig.ttsConfig.ignoreBackgroundNoise ?? false);
  const [allowInterrupt,        setAllowInterrupt]        = useState(voiceConfig.ttsConfig.allowInterrupt        ?? false);

  // ── Kokoro voice picker ───────────────────────────────────────────────────
  const [accentFilter,     setAccentFilter]     = useState("en-GB");
  const [selectedVoiceURI, setSelectedVoiceURI] = useState(voiceConfig.ttsConfig.voiceURI ?? "bm_george");
  const [serverVoice] = useState("en-AU-WilliamNeural");

  const kokoroVoices    = KOKORO_VOICES[accentFilter] ?? [];
  const voiceSelectData = kokoroVoices;

  // ── Form ──────────────────────────────────────────────────────────────────
  const form = useForm({
    initialValues: {
      characterName:     "Kevo",
      personality:       "a warm and professional luxury hospitality concierge for Karma Group, specialising in resort recommendations, membership benefits, booking experiences, spa and wellness, dining, and local activities across Asia, Europe, and beyond",
      voiceInstructions: "Speak in a warm, refined Australian accent — clear, calm, and welcoming, like a senior concierge at a world-class resort. Use natural rhythm and gentle pauses. Keep sentences short and conversational. Avoid sounding robotic or hurried.",
      promptName:        "Kevo — Voice & Character",
    },
    validate: {
      characterName: (v) => (v.trim() ? null : "Character name is required."),
      personality:   (v) => (v.trim() ? null : "Personality / traits are required."),
      promptName:    (v) => (v.trim() ? null : "Voice prompt name is required."),
    },
  });

  useEffect(() => {
    if (!manuallyEdited.current) {
      setPromptText(assemblePrompt(form.values.characterName, form.values.personality, form.values.voiceInstructions, rate, pitch, volume));
    }
  }, [form.values.characterName, form.values.personality, form.values.voiceInstructions]);

  const handleRate = (v: number) => {
    setSavedName(null); setRate(v);
    setPromptText((t) => injectTtsBlock(t, v, pitch, volume));
  };
  const handlePitch = (v: number) => {
    setSavedName(null); setPitch(v);
    setPromptText((t) => injectTtsBlock(t, rate, v, volume));
  };
  const handleVolume = (v: number) => {
    setSavedName(null); setVolume(v);
    setPromptText((t) => injectTtsBlock(t, rate, pitch, v));
  };

  const handlePromptTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setSavedName(null);
    const val = e.currentTarget.value;
    manuallyEdited.current = true;
    setPromptText(val);
    const parsed = parseTtsFromText(val);
    if (parsed) { setRate(parsed.rate); setPitch(parsed.pitch); setVolume(parsed.volume); }
  };

  const handleRegenerate = () => {
    manuallyEdited.current = false;
    setPromptText(assemblePrompt(form.values.characterName, form.values.personality, form.values.voiceInstructions, rate, pitch, volume));
  };

  const handleCharacterNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSavedName(null);
    const val = e.currentTarget.value;
    form.setFieldValue("characterName", val);
    if (!form.isDirty("promptName") || form.values.promptName === "")
      form.setFieldValue("promptName", val ? `${val} — Voice & Character` : "");
  };

  const [previewLoading, setPreviewLoading] = useState(false);

  const previewVoice = async (voiceURI?: string, _r?: number, _p?: number, _vol?: number) => {
    const voice = voiceURI ?? (serverVoice || selectedVoiceURI);
    if (!voice) {
      setError("Select a voice first before previewing.");
      return;
    }
    setPreviewLoading(true);
    setError(null);
    const apiUrl = import.meta.env.VITE_CHATBOT_API_URL ?? "http://localhost:8001";
    try {
      const res = await fetch(`${apiUrl}/api/v1/voice/preview?voice=${encodeURIComponent(voice)}`);
      if (!res.ok) {
        const text = await res.text();
        setError(`Preview failed (${res.status}): ${text.slice(0, 200)}`);
        return;
      }
      const data = await res.json();
      if (data.audio) {
        const mime = data.format === "mp3" ? "audio/mpeg" : `audio/${data.format ?? "wav"}`;
        const audio = new Audio(`data:${mime};base64,${data.audio}`);
        await audio.play();
      } else {
        setError("Backend returned no audio. Check that the Piper model downloaded successfully on startup.");
      }
    } catch (err: any) {
      setError(`Preview error: ${err?.message ?? `Could not reach backend — is it running on ${apiUrl}?`}`);
    } finally {
      setPreviewLoading(false);
    }
  };

  // ── Load voice prompt into form ───────────────────────────────────────────
  const handleLoadExperiment = (row: VoicePromptEntry) => {
    const data = parseVoicePrompt(row);
    if (!data) return;
    manuallyEdited.current = true;
    setPromptText(data.characterPrompt);
    const parsed = parseTtsFromText(data.characterPrompt);
    if (parsed) { setRate(parsed.rate); setPitch(parsed.pitch); setVolume(parsed.volume); }
    setSelectedVoiceURI(data.ttsConfig?.voiceURI ?? "");
    form.setValues({
      characterName:     data.characterName ?? "",
      personality:       "",
      voiceInstructions: "",
      promptName:        row.prompt_name,
    });
    setSavedName(null);
    window.scrollTo({ top: 0, behavior: "smooth" });
  };

  // ── Use in Production ─────────────────────────────────────────────────────
  // Only saves voice_character_prompt — voice TTS settings are managed
  // independently via Voice & Sliders "Use in Production".
  const handleUseInProduction = async (row: VoicePromptEntry) => {
    const data = parseVoicePrompt(row);
    if (!data) { setError("Cannot read this voice prompt's data."); return; }
    setPromotingId(row.id);
    setError(null);
    try {
      const charPayload = {
        prompt_name:  row.prompt_name,
        prompt_value: data.characterPrompt,
        prompt_key:   "voice_character_prompt",
      };
      const charRes: any = charPromptId
        ? await updatePrompt(charPromptId, charPayload)
        : await createPrompt(charPayload);

      if (charRes?.success === false) {
        setError(charRes?.message ?? "Failed to promote to production.");
        return;
      }

      if (!charPromptId && charRes?.data?.id) setCharPromptId(charRes.data.id);

      const newCharId = charPromptId || charRes?.data?.id;
      if (newCharId) {
        setProdRecords(prev => ({ ...prev, char: { id: newCharId, name: row.prompt_name, key: "voice_character_prompt" } }));
      }

      setLocalActiveChar(row.prompt_name);
      setProductionExpKey(row.prompt_key);
      markJustUpdated();
    } catch (err: any) {
      setError(err?.message ?? "Unexpected error.");
    } finally {
      setPromotingId(null);
    }
  };

  // ── Open draft for inline editing ────────────────────────────────────────
  const handleExpandDraft = (row: VoicePromptEntry) => {
    if (expandedId === row.id) { setExpandedId(null); return; }
    const data = parseVoicePrompt(row);
    setEditingText(data?.characterPrompt ?? row.prompt_value);
    setEditingName(row.prompt_name);
    setExpandedId(row.id);
  };

  // ── Save inline edits ─────────────────────────────────────────────────────
  const handleSaveEdit = async (row: VoicePromptEntry) => {
    if (!editingText.trim()) return;
    setSavingEditId(row.id);
    setError(null);
    const data = parseVoicePrompt(row);
    const updated: VoicePromptData = {
      characterPrompt: editingText.trim(),
      ttsConfig:       data?.ttsConfig ?? { rate, pitch, volume, voiceURI: selectedVoiceURI, voiceName: "", lang: accentFilter, genderHint: "male" },
      characterName:   data?.characterName ?? editingName,
    };
    try {
      const res: any = await updatePrompt(row.id, {
        prompt_name:  editingName.trim() || row.prompt_name,
        prompt_value: JSON.stringify(updated),
        prompt_key:   row.prompt_key,
      });
      if (res?.success === false) { setError(res?.message ?? "Failed to save."); return; }
      setVoicePrompts((prev) => prev.map((p) =>
        p.id === row.id
          ? { ...p, prompt_name: editingName.trim() || p.prompt_name, prompt_value: JSON.stringify(updated) }
          : p,
      ));
      setExpandedId(null);
    } catch (err: any) {
      setError(err?.message ?? "Unexpected error.");
    } finally {
      setSavingEditId(null);
    }
  };

  // ── Delete voice prompt ───────────────────────────────────────────────────
  const handleDeleteExperiment = async (row: VoicePromptEntry) => {
    if (!window.confirm(`Delete voice prompt "${row.prompt_name}"? This cannot be undone.`)) return;
    setDeletingId(row.id);
    setError(null);
    try {
      await deletePrompt(row.id);
      setVoicePrompts((prev) => prev.filter((e) => e.id !== row.id));
      if (productionExpKey === row.prompt_key) setProductionExpKey(null);
    } catch (err: any) {
      setError(err?.message ?? "Failed to delete voice prompt.");
    } finally {
      setDeletingId(null);
    }
  };

  // ── Delete voice text prompt ──────────────────────────────────────────────
  const handleDeleteTextPrompt = async (row: VoiceTextPrompt) => {
    if (!window.confirm(`Delete "${row.prompt_name}"? This cannot be undone.`)) return;
    setDeletingTextId(row.id);
    setError(null);
    try {
      await deletePrompt(row.id);
      setVoiceTextPrompts((prev) => prev.filter((r) => r.id !== row.id));
      // Remove from active set if it was active
      if (activeTextPromptIds.has(row.id)) {
        const next = new Set(activeTextPromptIds);
        next.delete(row.id);
        setActiveTextPromptIds(next);
      }
    } catch (err: any) {
      setError(err?.message ?? "Failed to delete.");
    } finally {
      setDeletingTextId(null);
    }
  };

  // ── Preview a voice testing prompt via Kokoro backend ───────────────────
  const previewTextPrompt = async (row: VoiceTextPrompt) => {
    const voice = serverVoice || selectedVoiceURI;
    if (!voice) { setError("Select a Chatbot Voice first."); return; }
    const text = row.prompt_value.slice(0, 200);
    const apiUrl = import.meta.env.VITE_CHATBOT_API_URL ?? "http://localhost:8001";
    try {
      const res = await fetch(`${apiUrl}/api/v1/voice/preview?voice=${encodeURIComponent(voice)}&text=${encodeURIComponent(text)}`);
      if (!res.ok) { setError(`Preview failed (${res.status})`); return; }
      const data = await res.json();
      if (data.audio) {
        const mime = data.format === "mp3" ? "audio/mpeg" : `audio/${data.format ?? "wav"}`;
        const audio = new Audio(`data:${mime};base64,${data.audio}`);
        await audio.play();
      } else {
        setError("Backend returned no audio. Check Piper model is available on the backend.");
      }
    } catch (err: any) {
      setError(`Preview error: ${err?.message ?? "Could not reach backend."}`);
    }
  };

  // ── Inline edit for voice testing prompts ────────────────────────────────
  const handleExpandTextPrompt = (row: VoiceTextPrompt) => {
    if (expandedTextId === row.id) { setExpandedTextId(null); return; }
    setEditingTextName(row.prompt_name);
    setEditingTextValue(row.prompt_value);
    setExpandedTextId(row.id);
  };

  const handleSaveTextEdit = async (row: VoiceTextPrompt) => {
    if (!editingTextValue.trim()) return;
    setSavingTextEditId(row.id);
    setError(null);
    try {
      const res: any = await updatePrompt(row.id, {
        prompt_name:  editingTextName.trim() || row.prompt_name,
        prompt_value: editingTextValue.trim(),
        prompt_key:   row.prompt_key,
      });
      if (res?.success === false) { setError(res?.message ?? "Failed to save."); return; }
      const updatedName = editingTextName.trim() || row.prompt_name;
      setVoiceTextPrompts((prev) => prev.map((r) =>
        r.id === row.id ? { ...r, prompt_name: updatedName, prompt_value: editingTextValue.trim() } : r,
      ));
      // If this prompt is active, refresh its entry in voice_active_text_prompts too
      if (activeTextPromptIds.has(row.id) && activeTextPromptConfigId) {
        const updatedActive = voiceTextPrompts
          .map((r) => r.id === row.id ? { ...r, prompt_name: updatedName, prompt_value: editingTextValue.trim() } : r)
          .filter((r) => activeTextPromptIds.has(r.id));
        await updatePrompt(activeTextPromptConfigId, {
          prompt_name:  "Voice Active Text Prompts",
          prompt_value: JSON.stringify(updatedActive.map((r) => ({
            id: r.id, prompt_key: r.prompt_key, prompt_name: r.prompt_name, prompt_value: r.prompt_value,
          }))),
          prompt_key: "voice_active_text_prompts",
        });
      }
      setExpandedTextId(null);
    } catch (err: any) {
      setError(err?.message ?? "Unexpected error.");
    } finally {
      setSavingTextEditId(null);
    }
  };

  // ── Toggle a voice testing prompt in / out of production ─────────────────
  // Multiple prompts can be active simultaneously — the full active set is
  // stored as JSON in the voice_active_text_prompts key.
  const handleToggleTextInProduction = async (row: VoiceTextPrompt) => {
    setTogglingTextId(row.id);
    setError(null);

    const next = new Set(activeTextPromptIds);
    if (next.has(row.id)) {
      next.delete(row.id);
    } else {
      next.add(row.id);
    }

    // Build the active prompts payload — list of { id, prompt_key, prompt_name, prompt_value }
    const activeRows = voiceTextPrompts.filter((r) => next.has(r.id));
    const payload = {
      prompt_name:  "Voice Active Text Prompts",
      prompt_value: JSON.stringify(activeRows.map((r) => ({
        id: r.id, prompt_key: r.prompt_key, prompt_name: r.prompt_name, prompt_value: r.prompt_value,
      }))),
      prompt_key: "voice_active_text_prompts",
    };

    try {
      const res: any = activeTextPromptConfigId
        ? await updatePrompt(activeTextPromptConfigId, payload)
        : await createPrompt(payload);
      if (res?.success === false) { setError(res?.message ?? "Failed to update active prompts."); return; }
      if (!activeTextPromptConfigId && res?.data?.id) setActiveTextPromptConfigId(res.data.id);
      setActiveTextPromptIds(next);
    } catch (err: any) {
      setError(err?.message ?? "Unexpected error.");
    } finally {
      setTogglingTextId(null);
    }
  };

  // ── Save recording config (draft) ────────────────────────────────────────
  const handleSaveRecordingConfig = async () => {
    setSavingRecording(true);
    setError(null);
    const config: VoiceRecordingConfig = {
      minRecordMs, silenceDuration, maxRecordMs, calibrationMs,
      noiseMarginDefault, noiseMarginHigh, bargeInFrames, levelFps, bargeInNoiseMargin,
      maxSpokenSentences, maxSpokenChars, disableCrawl, voiceLanguageModel,
    };
    const payload = {
      prompt_name:  "Voice Recording Config",
      prompt_value: JSON.stringify(config),
      prompt_key:   "voice_recording_config",
    };
    try {
      const res: any = recordingConfigId
        ? await updatePrompt(recordingConfigId, payload)
        : await createPrompt(payload);
      if (res?.success === false) { setError(res?.message ?? "Failed to save recording config."); return; }
      if (!recordingConfigId && res?.data?.id) setRecordingConfigId(res.data.id);
      setSavedRecording(true);
      setTimeout(() => setSavedRecording(false), 3000);
    } catch (err: any) {
      setError(err?.message ?? "Unexpected error.");
    } finally {
      setSavingRecording(false);
    }
  };

  // ── Use Recording Settings in Production ──────────────────────────────────
  // Saves both voice_recording_config (timing/noise constants) AND updates
  // voice_tts_config with the current ignoreBackgroundNoise + allowInterrupt flags.
  const [promotingRecording, setPromotingRecording] = useState(false);
  const [promotingSliders,   setPromotingSliders]   = useState(false);

  // Snapshots of what is currently live in production.
  // Initialised from DB on page load; updated after every successful push.
  // Comparing current form values against the snapshot drives the "In Production" badge.
  const [prodTss, setProdTss] = useState({
    exists:               !!voiceConfig.ttsConfigId,
    rate:                 voiceConfig.ttsConfig.rate,
    pitch:                voiceConfig.ttsConfig.pitch,
    volume:               voiceConfig.ttsConfig.volume,
    voiceURI:             voiceConfig.ttsConfig.voiceURI ?? "",
    ignoreBackgroundNoise: voiceConfig.ttsConfig.ignoreBackgroundNoise ?? false,
    allowInterrupt:       voiceConfig.ttsConfig.allowInterrupt ?? false,
  });

  const [prodRec, setProdRec] = useState({
    exists:              !!voiceConfig.recordingConfigId,
    minRecordMs:         voiceConfig.recordingConfig.minRecordMs,
    silenceDuration:     voiceConfig.recordingConfig.silenceDuration,
    maxRecordMs:         voiceConfig.recordingConfig.maxRecordMs,
    calibrationMs:       voiceConfig.recordingConfig.calibrationMs,
    noiseMarginDefault:  voiceConfig.recordingConfig.noiseMarginDefault,
    noiseMarginHigh:     voiceConfig.recordingConfig.noiseMarginHigh,
    bargeInFrames:       voiceConfig.recordingConfig.bargeInFrames,
    levelFps:            voiceConfig.recordingConfig.levelFps,
    bargeInNoiseMargin:  voiceConfig.recordingConfig.bargeInNoiseMargin  ?? 10,
    maxSpokenSentences:  voiceConfig.recordingConfig.maxSpokenSentences  ?? 2,
    maxSpokenChars:      voiceConfig.recordingConfig.maxSpokenChars      ?? 250,
    disableCrawl:        voiceConfig.recordingConfig.disableCrawl        ?? true,
    voiceLanguageModel:  voiceConfig.recordingConfig.voiceLanguageModel  ?? "gemini-2.0-flash-lite",
  });

  // Derived: true when the current form values exactly match what is live in production
  const slidersMatchProduction =
    prodTss.exists &&
    rate === prodTss.rate &&
    pitch === prodTss.pitch &&
    volume === prodTss.volume &&
    selectedVoiceURI === prodTss.voiceURI &&
    ignoreBackgroundNoise === prodTss.ignoreBackgroundNoise &&
    allowInterrupt === prodTss.allowInterrupt;

  const recordingMatchProduction =
    prodRec.exists &&
    minRecordMs          === prodRec.minRecordMs &&
    silenceDuration      === prodRec.silenceDuration &&
    maxRecordMs          === prodRec.maxRecordMs &&
    calibrationMs        === prodRec.calibrationMs &&
    noiseMarginDefault   === prodRec.noiseMarginDefault &&
    noiseMarginHigh      === prodRec.noiseMarginHigh &&
    bargeInFrames        === prodRec.bargeInFrames &&
    levelFps             === prodRec.levelFps &&
    bargeInNoiseMargin   === prodRec.bargeInNoiseMargin &&
    maxSpokenSentences   === prodRec.maxSpokenSentences &&
    maxSpokenChars       === prodRec.maxSpokenChars &&
    disableCrawl         === prodRec.disableCrawl &&
    voiceLanguageModel   === prodRec.voiceLanguageModel;

  const handleUseRecordingInProduction = async () => {
    if (!serverVoice.trim()) {
      setError("Please select a Chatbot Voice before pushing to production. The browser voice picker only affects your local preview — chatbot users will hear the default voice until you pick one here.");
      return;
    }
    setPromotingRecording(true);
    setError(null);

    const recordingConfigPayload = {
      prompt_name:  "Voice Recording Config",
      prompt_value: JSON.stringify({
        minRecordMs, silenceDuration, maxRecordMs, calibrationMs,
        noiseMarginDefault, noiseMarginHigh, bargeInFrames, levelFps, bargeInNoiseMargin,
        maxSpokenSentences, maxSpokenChars, disableCrawl, voiceLanguageModel,
      } as VoiceRecordingConfig),
      prompt_key: "voice_recording_config",
    };

    // Merge behaviour flags into the existing TTS config (keep all other fields)
    const selectedVoice = kokoroVoices.find((v) => v.value === selectedVoiceURI);
    const ttsConfigPayload = {
      prompt_name:  "Voice TTS Config",
      prompt_value: JSON.stringify({
        rate, pitch, volume,
        voiceURI:             selectedVoiceURI,
        voiceName:            selectedVoice?.label ?? voiceConfig.ttsConfig.voiceName ?? "",
        lang:                 accentFilter,
        genderHint:           "male" as const,
        ignoreBackgroundNoise,
        allowInterrupt,
        serverVoice:          serverVoice.trim(),
      }),
      prompt_key: "voice_tts_config",
    };

    try {
      const [recRes, ttsRes]: any[] = await Promise.all([
        recordingConfigId
          ? updatePrompt(recordingConfigId, recordingConfigPayload)
          : createPrompt(recordingConfigPayload),
        ttsConfigId
          ? updatePrompt(ttsConfigId, ttsConfigPayload)
          : createPrompt(ttsConfigPayload),
      ]);

      if (recRes?.success === false || ttsRes?.success === false) {
        setError(recRes?.message ?? ttsRes?.message ?? "Failed to push to production.");
        return;
      }
      if (!recordingConfigId && recRes?.data?.id) setRecordingConfigId(recRes.data.id);
      if (!ttsConfigId && ttsRes?.data?.id) setTtsConfigId(ttsRes.data.id);

      setProdRec({ exists: true, minRecordMs, silenceDuration, maxRecordMs, calibrationMs, noiseMarginDefault, noiseMarginHigh, bargeInFrames, levelFps, bargeInNoiseMargin, maxSpokenSentences, maxSpokenChars, disableCrawl, voiceLanguageModel });
      setProdTss({ exists: true, rate, pitch, volume, voiceURI: selectedVoiceURI, ignoreBackgroundNoise, allowInterrupt });
      setLocalActiveVoice(serverVoice.trim() || localActiveVoice);
      const newRecId = recordingConfigId || recRes?.data?.id;
      if (newRecId) {
        setProdRecords(prev => ({ ...prev, recording: { id: newRecId, name: "Voice Recording Config", key: "voice_recording_config" } }));
      }
      markJustUpdated();
    } catch (err: any) {
      setError(err?.message ?? "Unexpected error.");
    } finally {
      setPromotingRecording(false);
    }
  };

  // ── Use Voice & Sliders in Production ────────────────────────────────────
  // Pushes the current rate/pitch/volume/voice/behaviour flags to voice_tts_config only.
  const handleUseSlidersInProduction = async () => {
    if (!serverVoice.trim()) {
      setError("Please select a Chatbot Voice before pushing to production. The browser voice picker only affects your local preview — chatbot users will hear the default voice until you pick one here.");
      return;
    }
    setPromotingSliders(true);
    setError(null);
    const selectedVoice = kokoroVoices.find((v) => v.value === selectedVoiceURI);
    const payload = {
      prompt_name:  "Voice TTS Config",
      prompt_value: JSON.stringify({
        rate, pitch, volume,
        voiceURI:             selectedVoiceURI,
        voiceName:            selectedVoice?.label ?? voiceConfig.ttsConfig.voiceName ?? "",
        lang:                 accentFilter,
        genderHint:           "male" as const,
        ignoreBackgroundNoise,
        allowInterrupt,
        serverVoice:          serverVoice.trim(),
      }),
      prompt_key: "voice_tts_config",
    };
    try {
      const res: any = ttsConfigId
        ? await updatePrompt(ttsConfigId, payload)
        : await createPrompt(payload);
      if (res?.success === false) { setError(res?.message ?? "Failed to push sliders to production."); return; }
      if (!ttsConfigId && res?.data?.id) setTtsConfigId(res.data.id);
      setProdTss({ exists: true, rate, pitch, volume, voiceURI: selectedVoiceURI, ignoreBackgroundNoise, allowInterrupt });
      setLocalActiveVoice(serverVoice.trim() || localActiveVoice);
      const newTtsId = ttsConfigId || res?.data?.id;
      if (newTtsId) {
        setProdRecords(prev => ({ ...prev, tts: { id: newTtsId, name: "Voice TTS Config", key: "voice_tts_config" } }));
      }
      markJustUpdated();
    } catch (err: any) {
      setError(err?.message ?? "Unexpected error.");
    } finally {
      setPromotingSliders(false);
    }
  };

  // ── Delete a production record ────────────────────────────────────────────
  const handleDeleteProdRecord = async (key: "tts" | "char" | "recording" | "active") => {
    const rec = prodRecords[key];
    if (!rec) return;
    if (!window.confirm(`Delete production record "${rec.name}" (${rec.key})?\n\nThis will remove the live config. The chatbot will fall back to defaults until a new one is pushed.`)) return;
    setDeletingProdKey(key);
    setError(null);
    try {
      await deletePrompt(rec.id);
      setProdRecords((prev) => ({ ...prev, [key]: null }));
      // Clear the matching mutable ID so next "Use in Production" creates a new record
      if (key === "tts")       setTtsConfigId(null);
      if (key === "char")      setCharPromptId(null);
      if (key === "recording") setRecordingConfigId(null);
    } catch (err: any) {
      setError(err?.message ?? "Failed to delete production record.");
    } finally {
      setDeletingProdKey(null);
    }
  };


  // ── Save as new voice prompt ──────────────────────────────────────────────
  const handleSubmit = form.onSubmit(async (values) => {
    if (!promptText.trim()) { setError("Prompt text cannot be empty."); return; }
    setError(null);
    setLoading(true);

    const selectedVoice = kokoroVoices.find((v) => v.value === selectedVoiceURI);
    const ttsConfig = {
      ...(parseTtsFromText(promptText) ?? { rate, pitch, volume }),
      voiceURI:              selectedVoiceURI,
      voiceName:             selectedVoice?.label ?? "",
      lang:                  accentFilter,
      genderHint:            "male" as const,
      ignoreBackgroundNoise,
      allowInterrupt,
    };

    const promptData: VoicePromptData = {
      characterPrompt: promptText.trim(),
      ttsConfig,
      characterName:   values.characterName.trim(),
    };

    const promptKey = `voice_exp_${Date.now()}`;

    try {
      const res: any = await createPrompt({
        prompt_name:  values.promptName.trim(),
        prompt_value: JSON.stringify(promptData),
        prompt_key:   promptKey,
      });

      if (res?.success === false) { setError(res?.message ?? "Failed to save voice prompt."); return; }

      const newRow: VoicePromptEntry = {
        id:           res?.data?.id ?? String(Date.now()),
        prompt_name:  values.promptName.trim(),
        prompt_key:   promptKey,
        prompt_value: JSON.stringify(promptData),
      };
      setVoicePrompts((prev) => [newRow, ...prev]);
      setSavedName(values.promptName.trim());
    } catch (err: any) {
      setError(err?.message ?? "An unexpected error occurred.");
    } finally {
      setLoading(false);
    }
  });

  // ── VAPI-style preset ─────────────────────────────────────────────────────
  // Pre-fills Recording Settings + Behaviour toggles with values that mirror
  // VAPI's recommended defaults: fast barge-in, short silence, higher noise
  // margins, and both behaviour flags enabled. Click "Use in Production" after.
  const applyVapiPreset = () => {
    setAllowInterrupt(true);
    setIgnoreBackgroundNoise(false);
    setMinRecordMs(500);
    setSilenceDuration(1000);
    setMaxRecordMs(25000);
    setCalibrationMs(300);
    setNoiseMarginDefault(10);
    setNoiseMarginHigh(25);
    setBargeInFrames(1);
    setLevelFps(20);
    setBargeInNoiseMargin(6);
    setDisableCrawl(true);
    setMaxSpokenSentences(2);
    setMaxSpokenChars(200);
    setVoiceLanguageModel("gemini-2.0-flash-lite");
  };

  // ── Derived display lists ─────────────────────────────────────────────────
  // Only non-active prompts stay in the Voice Testing table.
  // Active ones move to Production Records.
  const displayTestingPrompts  = voiceTextPrompts.filter(r => !activeTextPromptIds.has(r.id));
  const productionTextPrompts  = voiceTextPrompts.filter(r =>  activeTextPromptIds.has(r.id));
  const hasProductionContent   = !!(prodRecords.char || productionTextPrompts.length > 0);

  // ── Render ────────────────────────────────────────────────────────────────
  return (
    <Stack gap="md">

      {/* ── Production Records ─────────────────────────────────────────────── */}
      {/* Shows: character prompt + each active voice testing prompt.           */}
      {/* voice_tts_config and voice_recording_config are managed by their own  */}
      {/* form sections (trash icons there); they do not appear in this table.  */}
      {hasProductionContent && (
        <Paper withBorder p="md">
          <Stack gap="sm">
            <Group justify="space-between" align="center">
              <Group gap="xs">
                <Text fw={600} size="sm">Production Records</Text>
                <Badge size="xs" color="green" variant="filled">Live</Badge>
                {productionTextPrompts.length > 0 && (
                  <Badge size="xs" color="teal" variant="light">
                    {productionTextPrompts.length} text prompt{productionTextPrompts.length > 1 ? "s" : ""} active
                  </Badge>
                )}
                {localActiveChar && (
                  <Badge size="xs" color="violet" variant="light">{localActiveChar}</Badge>
                )}
                {localActiveVoice && (
                  <Badge size="xs" color="blue" variant="light">{localActiveVoice}</Badge>
                )}
                {justUpdated && (
                  <Badge size="xs" color="green" variant="dot">Just updated</Badge>
                )}
              </Group>
              <Text size="xs" c="dimmed">Live config in the chatbot. Remove a text prompt to send it back to Voice Testing.</Text>
            </Group>
            <Table fz={12} withColumnBorders verticalSpacing="xs">
              <Table.Thead>
                <Table.Tr bg="gray.1" c="dark.4">
                  <Table.Th px={10}>Name</Table.Th>
                  <Table.Th px={10}>Pipeline Key</Table.Th>
                  <Table.Th px={10}>Type</Table.Th>
                  <Table.Th px={10}>Actions</Table.Th>
                </Table.Tr>
              </Table.Thead>
              <Table.Tbody>
                {/* Character prompt row */}
                {prodRecords.char && (
                  <Table.Tr key="char">
                    <Table.Td px={10}><Text size="xs" fw={500}>{prodRecords.char.name}</Text></Table.Td>
                    <Table.Td px={10}><Badge size="xs" color="green" variant="light">{prodRecords.char.key}</Badge></Table.Td>
                    <Table.Td px={10}><Badge size="xs" color="violet" variant="light">Character</Badge></Table.Td>
                    <Table.Td px={10}>
                      <Group gap={4} wrap="nowrap">
                        <Tooltip label="Edit this record" withArrow>
                          <ActionIcon size="sm" variant="light" color="blue"
                            onClick={() => navigate(`edit/${prodRecords.char!.id}`)}>
                            <IconEdit size={13} />
                          </ActionIcon>
                        </Tooltip>
                        <Tooltip label="Delete from production" withArrow>
                          <ActionIcon size="sm" variant="light" color="red"
                            loading={deletingProdKey === "char"}
                            onClick={() => handleDeleteProdRecord("char")}>
                            <IconTrash size={13} />
                          </ActionIcon>
                        </Tooltip>
                      </Group>
                    </Table.Td>
                  </Table.Tr>
                )}

                {/* Active voice testing prompt rows — each is individually editable */}
                {productionTextPrompts.map((row) => {
                  const isExpanded = expandedTextId === row.id;
                  return (
                    <>
                      <Table.Tr
                        key={row.id}
                        style={{ cursor: "pointer", background: isExpanded ? "var(--mantine-color-blue-0)" : "var(--mantine-color-green-0)" }}
                        onClick={() => handleExpandTextPrompt(row)}
                      >
                        <Table.Td px={10}>
                          <Group gap={6}>
                            <Text size="xs" fw={500}>{row.prompt_name}</Text>
                            {isExpanded && <Badge size="xs" color="blue" variant="light">editing</Badge>}
                          </Group>
                        </Table.Td>
                        <Table.Td px={10}><Badge size="xs" color="gray" variant="light">{row.prompt_key}</Badge></Table.Td>
                        <Table.Td px={10}><Badge size="xs" color="teal" variant="light">Testing Prompt</Badge></Table.Td>
                        <Table.Td px={10} onClick={(e) => e.stopPropagation()}>
                          <Group gap={4} wrap="nowrap">
                            <Tooltip label="Preview via TTS" withArrow>
                              <ActionIcon size="sm" variant="light" color="violet"
                                onClick={(e) => { e.stopPropagation(); previewTextPrompt(row); }}>
                                <IconPlayerPlay size={13} />
                              </ActionIcon>
                            </Tooltip>
                            <Tooltip label={isExpanded ? "Close editor" : "Edit inline"} withArrow>
                              <ActionIcon size="sm" variant={isExpanded ? "filled" : "light"} color="blue"
                                onClick={(e) => { e.stopPropagation(); handleExpandTextPrompt(row); }}>
                                <IconEdit size={13} />
                              </ActionIcon>
                            </Tooltip>
                            <Tooltip label="Remove from production (sends back to Voice Testing)" withArrow>
                              <ActionIcon size="sm" variant="light" color="orange"
                                loading={togglingTextId === row.id}
                                onClick={(e) => { e.stopPropagation(); handleToggleTextInProduction(row); }}>
                                <IconTrash size={13} />
                              </ActionIcon>
                            </Tooltip>
                          </Group>
                        </Table.Td>
                      </Table.Tr>

                      {/* Inline editor for production text prompt */}
                      {isExpanded && (
                        <Table.Tr key={`${row.id}-prod-edit`} style={{ background: "var(--mantine-color-blue-0)" }}>
                          <Table.Td colSpan={4} px={12} py={12}>
                            <Stack gap="sm">
                              <TextInput
                                label="Prompt Name"
                                size="xs"
                                value={editingTextName}
                                onChange={(e) => setEditingTextName(e.currentTarget.value)}
                                styles={{ input: { fontWeight: 500 } }}
                              />
                              <Textarea
                                label="Prompt Text"
                                description="Edit the voice instruction. Changes are saved to production immediately."
                                value={editingTextValue}
                                onChange={(e) => setEditingTextValue(e.currentTarget.value)}
                                minRows={6}
                                autosize
                                size="xs"
                                styles={{ input: { fontFamily: "monospace" } }}
                              />
                              <Group justify="flex-end" gap="xs">
                                <Button size="xs" variant="subtle" color="gray"
                                  onClick={() => setExpandedTextId(null)}>
                                  Cancel
                                </Button>
                                <Button size="xs" color="blue"
                                  loading={savingTextEditId === row.id}
                                  onClick={() => handleSaveTextEdit(row)}>
                                  Save Changes
                                </Button>
                              </Group>
                            </Stack>
                          </Table.Td>
                        </Table.Tr>
                      )}
                    </>
                  );
                })}
              </Table.Tbody>
            </Table>
          </Stack>
        </Paper>
      )}

      {/* ── Saved voice prompt confirmation ── */}
      {savedName && (
        <Paper p="md" withBorder style={{ borderColor: "var(--mantine-color-teal-5)", background: "var(--mantine-color-teal-0)" }}>
          <Group gap="sm">
            <ThemeIcon color="teal" variant="filled" size="md" radius="xl">
              <IconCheck size={14} />
            </ThemeIcon>
            <Stack gap={2}>
              <Text size="sm" fw={700} c="teal.8">Voice prompt saved: "{savedName}"</Text>
              <Text size="xs" c="dimmed">
                It appears in the list below. Click <b>Use in Production</b> to make it the active chatbot voice.
              </Text>
            </Stack>
          </Group>
        </Paper>
      )}

      {/* ── Voice Testing Prompts table ── */}
      {/* Only shows prompts NOT yet in production. Once activated via the      */}
      {/* rocket button, the prompt moves up into Production Records above.     */}
      <Paper withBorder p="md">
        <Stack gap="sm">
          <Group justify="space-between" align="center">
            <Group gap="xs">
              <Text fw={600} size="sm">
                Voice Testing Prompts ({displayTestingPrompts.length})
              </Text>
              {activeTextPromptIds.size > 0 && (
                <Badge size="sm" color="green" variant="filled">
                  {activeTextPromptIds.size} active in production
                </Badge>
              )}
            </Group>
            <Button size="xs" leftSection={<IconPlus size={13} />} onClick={() => navigate("create")}>
              New Voice Testing Prompt
            </Button>
          </Group>

          {displayTestingPrompts.length === 0 ? (
            <Text size="xs" c="dimmed" ta="center" py="md">
              {voiceTextPrompts.length === 0
                ? <>No voice testing prompts yet. Click "New Voice Testing Prompt" to create one. Use a key starting with <Badge size="xs" color="gray" variant="light">voice_</Badge> to keep it in this tab.</>
                : "All prompts are in production. Check Production Records above."}
            </Text>
          ) : (
            <Table fz={12} highlightOnHover verticalSpacing="xs" withColumnBorders>
              <Table.Thead>
                <Table.Tr bg="gray.1" c="dark.4">
                  <Table.Th px={10}>Name</Table.Th>
                  <Table.Th px={10}>Key</Table.Th>
                  <Table.Th px={10}>Preview</Table.Th>
                  <Table.Th px={10}>Created By</Table.Th>
                  <Table.Th px={10}>Updated By</Table.Th>
                  <Table.Th px={10}>Actions</Table.Th>
                </Table.Tr>
              </Table.Thead>
              <Table.Tbody>
                {displayTestingPrompts.map((row) => {
                  const isExpanded = expandedTextId === row.id;
                  return (
                    <>
                      <Table.Tr
                        key={row.id}
                        style={{ cursor: "pointer", background: isExpanded ? "var(--mantine-color-blue-0)" : undefined }}
                        onClick={() => handleExpandTextPrompt(row)}
                      >
                        <Table.Td px={10}>
                          <Group gap={6}>
                            <Text size="xs" fw={500}>{row.prompt_name}</Text>
                            {isExpanded && <Badge size="xs" color="blue" variant="light">editing</Badge>}
                          </Group>
                        </Table.Td>
                        <Table.Td px={10}>
                          <Badge size="xs" color="gray" variant="light">{row.prompt_key}</Badge>
                        </Table.Td>
                        <Table.Td px={10}>
                          <Text size="xs" c="dimmed" lineClamp={2} style={{ maxWidth: 260 }}>
                            {row.prompt_value}
                          </Text>
                        </Table.Td>
                        <Table.Td px={10}><Text size="xs" c="dimmed">{row.created_by || "—"}</Text></Table.Td>
                        <Table.Td px={10}><Text size="xs" c="dimmed">{row.updated_by || "—"}</Text></Table.Td>
                        <Table.Td px={10} onClick={(e) => e.stopPropagation()}>
                          <Group gap={4} wrap="nowrap">
                            <Tooltip label="Preview (speak prompt text)" withArrow>
                              <ActionIcon size="sm" variant="light" color="violet"
                                onClick={(e) => { e.stopPropagation(); previewTextPrompt(row); }}>
                                <IconPlayerPlay size={13} />
                              </ActionIcon>
                            </Tooltip>
                            <Tooltip label={isExpanded ? "Close editor" : "Edit inline"} withArrow>
                              <ActionIcon size="sm" variant={isExpanded ? "filled" : "light"} color="blue"
                                onClick={(e) => { e.stopPropagation(); handleExpandTextPrompt(row); }}>
                                <IconEdit size={13} />
                              </ActionIcon>
                            </Tooltip>
                            <Tooltip label="Use in Production — moves this prompt to Production Records" withArrow>
                              <ActionIcon
                                size="sm"
                                variant="light"
                                color="green"
                                loading={togglingTextId === row.id}
                                onClick={(e) => { e.stopPropagation(); handleToggleTextInProduction(row); }}
                              >
                                <IconRocket size={13} />
                              </ActionIcon>
                            </Tooltip>
                            <Tooltip label="Delete" withArrow>
                              <ActionIcon size="sm" variant="light" color="red"
                                loading={deletingTextId === row.id}
                                onClick={(e) => { e.stopPropagation(); handleDeleteTextPrompt(row); }}>
                                <IconTrash size={13} />
                              </ActionIcon>
                            </Tooltip>
                          </Group>
                        </Table.Td>
                      </Table.Tr>

                      {/* Inline editor row */}
                      {isExpanded && (
                        <Table.Tr key={`${row.id}-edit`} style={{ background: "var(--mantine-color-blue-0)" }}>
                          <Table.Td colSpan={6} px={12} py={12}>
                            <Stack gap="sm">
                              <TextInput
                                label="Prompt Name"
                                size="xs"
                                value={editingTextName}
                                onChange={(e) => setEditingTextName(e.currentTarget.value)}
                                styles={{ input: { fontWeight: 500 } }}
                              />
                              <Textarea
                                label="Prompt Text"
                                description="Edit the voice instruction text. Use VOICE RESTRICTION: or IMPORTANT: for rules the bot must follow."
                                value={editingTextValue}
                                onChange={(e) => setEditingTextValue(e.currentTarget.value)}
                                minRows={6}
                                autosize
                                size="xs"
                                styles={{ input: { fontFamily: "monospace" } }}
                              />
                              <Group justify="flex-end" gap="xs">
                                <Button size="xs" variant="subtle" color="gray"
                                  onClick={() => setExpandedTextId(null)}>
                                  Cancel
                                </Button>
                                <Button size="xs" color="blue"
                                  loading={savingTextEditId === row.id}
                                  onClick={() => handleSaveTextEdit(row)}>
                                  Save Changes
                                </Button>
                              </Group>
                            </Stack>
                          </Table.Td>
                        </Table.Tr>
                      )}
                    </>
                  );
                })}
              </Table.Tbody>
            </Table>
          )}
        </Stack>
      </Paper>

      {/* ── Saved Voice Prompts list (expandable) ── */}
      {voicePrompts.length > 0 && (
        <Paper withBorder p="md">
          <Stack gap="sm">
            <Text fw={600} size="sm">
              Voice &amp; Character Prompts ({voicePrompts.length})
            </Text>
            <Text size="xs" c="dimmed">Click a row or the edit icon to view and update the character prompt. "Use in Production" promotes character personality only — voice settings are managed separately via Voice & Sliders.</Text>
            <Table fz={12} verticalSpacing="xs" withColumnBorders>
              <Table.Thead>
                <Table.Tr bg="gray.1" c="dark.4">
                  <Table.Th px={10}>Name / Character</Table.Th>
                  <Table.Th px={10}>Voice</Table.Th>
                  <Table.Th px={10}>Speed</Table.Th>
                  <Table.Th px={10}>Pitch</Table.Th>
                  <Table.Th px={10}>Status</Table.Th>
                  <Table.Th px={10}>Actions</Table.Th>
                </Table.Tr>
              </Table.Thead>
              <Table.Tbody>
                {voicePrompts.map((row) => {
                  const data         = parseVoicePrompt(row);
                  const isProduction = row.prompt_key === productionExpKey;
                  const isExpanded   = expandedId === row.id;
                  return (
                    <>
                      {/* ── Summary row ── */}
                      <Table.Tr
                        key={row.id}
                        style={{ cursor: "pointer", background: isExpanded ? "var(--mantine-color-blue-0)" : undefined }}
                        onClick={() => handleExpandDraft(row)}
                      >
                        <Table.Td px={10}>
                          <Group gap={6}>
                            <Text size="xs" fw={500}>{row.prompt_name}</Text>
                            {isExpanded && <Badge size="xs" color="blue" variant="light">editing</Badge>}
                          </Group>
                          {data?.characterName && (
                            <Text size="xs" c="dimmed">{data.characterName}</Text>
                          )}
                        </Table.Td>
                        <Table.Td px={10}>
                          <Text size="xs" c="dimmed" lineClamp={1} style={{ maxWidth: 160 }}>
                            {data?.ttsConfig?.voiceName || "—"}
                          </Text>
                        </Table.Td>
                        <Table.Td px={10}>
                          <Text size="xs">{data?.ttsConfig?.rate?.toFixed(1) ?? "—"}×</Text>
                        </Table.Td>
                        <Table.Td px={10}>
                          <Text size="xs">{data?.ttsConfig?.pitch?.toFixed(1) ?? "—"}</Text>
                        </Table.Td>
                        <Table.Td px={10}>
                          {isProduction
                            ? <Badge size="xs" color="green" variant="filled">In Production</Badge>
                            : <Badge size="xs" color="gray" variant="light">Draft</Badge>}
                        </Table.Td>
                        <Table.Td px={10} onClick={(e) => e.stopPropagation()}>
                          <Group gap={4} wrap="nowrap">
                            <Tooltip label="Edit prompt" withArrow>
                              <ActionIcon size="sm" variant="light" color="blue"
                                onClick={(e) => { e.stopPropagation(); handleExpandDraft(row); }}>
                                <IconEdit size={13} />
                              </ActionIcon>
                            </Tooltip>
                            <Tooltip label="Preview voice" withArrow>
                              <ActionIcon size="sm" variant="light" color="violet"
                                onClick={() => {
                                  if (!data?.ttsConfig) return;
                                  previewVoice(data.ttsConfig.voiceURI, data.ttsConfig.rate, data.ttsConfig.pitch, data.ttsConfig.volume);
                                }}>
                                <IconPlayerPlay size={13} />
                              </ActionIcon>
                            </Tooltip>
                            <Tooltip label={isProduction ? "Already in production" : "Use in Production"} withArrow>
                              <ActionIcon size="sm" variant="filled" color="green"
                                loading={promotingId === row.id}
                                disabled={isProduction}
                                onClick={() => handleUseInProduction(row)}>
                                <IconRocket size={13} />
                              </ActionIcon>
                            </Tooltip>
                            <Tooltip label="Delete" withArrow>
                              <ActionIcon size="sm" variant="light" color="red"
                                loading={deletingId === row.id}
                                onClick={() => handleDeleteExperiment(row)}>
                                <IconTrash size={13} />
                              </ActionIcon>
                            </Tooltip>
                          </Group>
                        </Table.Td>
                      </Table.Tr>

                      {/* ── Expanded editor row ── */}
                      {isExpanded && (
                        <Table.Tr key={`${row.id}-edit`} style={{ background: "var(--mantine-color-blue-0)" }}>
                          <Table.Td colSpan={6} px={12} py={12}>
                            <Stack gap="sm">
                              <TextInput
                                label="Prompt Name"
                                size="xs"
                                value={editingName}
                                onChange={(e) => setEditingName(e.currentTarget.value)}
                                styles={{ input: { fontWeight: 500 } }}
                              />
                              <Textarea
                                label="Prompt Text"
                                description="Edit the character prompt below. The TTS settings (rate/pitch/volume) are preserved."
                                value={editingText}
                                onChange={(e) => setEditingText(e.currentTarget.value)}
                                minRows={10}
                                autosize
                                size="xs"
                                styles={{ input: { fontFamily: "monospace" } }}
                              />
                              <Group justify="flex-end" gap="xs">
                                <Button size="xs" variant="subtle" color="gray"
                                  onClick={() => setExpandedId(null)}>
                                  Cancel
                                </Button>
                                <Button size="xs" color="blue"
                                  loading={savingEditId === row.id}
                                  onClick={() => handleSaveEdit(row)}>
                                  Save Changes
                                </Button>
                              </Group>
                            </Stack>
                          </Table.Td>
                        </Table.Tr>
                      )}
                    </>
                  );
                })}
              </Table.Tbody>
            </Table>
          </Stack>
        </Paper>
      )}

      {/* ── Config form ── */}
      <form onSubmit={handleSubmit}>
        <Stack gap="md">

          {/* Character details */}
          <Paper p="md" withBorder>
            <Stack gap="sm">
              <Text fw={600} size="sm">
                <IconMicrophone size={14} style={{ marginRight: 6, verticalAlign: "middle" }} />
                Character Details
              </Text>
              <TextInput
                label="Character Name"
                placeholder="e.g. Karma"
                size="sm"
                {...form.getInputProps("characterName")}
                onChange={handleCharacterNameChange}
              />
              <Textarea
                label="Personality & Traits"
                placeholder="e.g. a friendly and professional hospitality assistant with a warm and welcoming personality"
                description="Completes: 'You are [Name], ...'"
                size="sm" minRows={3} autosize
                {...form.getInputProps("personality")}
                onChange={(e) => { setSavedName(null); form.getInputProps("personality").onChange(e); }}
              />
              <Textarea
                label="Voice & Accent Instructions"
                placeholder={`Write how this character sounds.\n\ne.g. Speak in a warm South African voice. Use expressions like 'lekker', 'eish', 'sharp', 'howzit' naturally.`}
                description="Appears under '## Accent & Character Voice' in the prompt."
                size="sm" minRows={4} autosize
                {...form.getInputProps("voiceInstructions")}
                onChange={(e) => { setSavedName(null); form.getInputProps("voiceInstructions").onChange(e); }}
              />
            </Stack>
          </Paper>

          {/* Voice & TTS */}
          <Paper p="md" withBorder>
            <Stack gap="lg">
              <Group justify="space-between" align="center">
                <div>
                  <Text fw={600} size="sm">
                    <IconVolume size={14} style={{ marginRight: 6, verticalAlign: "middle" }} />
                    Voice & Sliders
                  </Text>
                </div>
                <Group gap="xs">
                  <Button size="xs" variant="light" color="violet" loading={previewLoading} onClick={() => previewVoice()}>
                    Preview Voice
                  </Button>
                  {ttsConfigId && (
                    <Tooltip label="Delete the voice_tts_config production record" withArrow>
                      <ActionIcon
                        size="sm" variant="light" color="red"
                        loading={deletingProdKey === "tts"}
                        onClick={() => handleDeleteProdRecord("tts")}
                      >
                        <IconTrash size={13} />
                      </ActionIcon>
                    </Tooltip>
                  )}
                  <Button
                    size="xs"
                    variant={slidersMatchProduction ? "light" : "filled"}
                    color={slidersMatchProduction ? "teal" : "green"}
                    loading={promotingSliders}
                    leftSection={slidersMatchProduction ? <IconCheck size={13} /> : <IconRocket size={13} />}
                    onClick={handleUseSlidersInProduction}
                  >
                    {slidersMatchProduction ? "In Production" : "Use in Production"}
                  </Button>
                </Group>
              </Group>

              {/* ── Server Voice — fixed to Bartell (Australian Male, Piper local) ── */}
              <Stack gap={6}>
                <Text size="sm" fw={500}>Chatbot Voice</Text>
                <Group gap="sm" align="center">
                  <Badge size="lg" color="blue" variant="light">William (Australian Male)</Badge>
                  <Text size="xs" c="dimmed">en-AU-WilliamNeural · edge-tts (Microsoft) · no API key required</Text>
                </Group>
                <Text size="xs" c="dimmed">This voice is used for all chatbot audio — greeting and every bot response. Adjust Speed and Pitch above to tune the pronunciation.</Text>
              </Stack>

              <Stack gap="xs">
                <Group justify="space-between">
                  <Text size="sm" fw={500}>Speed</Text>
                  <Text size="xs" c="dimmed">{rate.toFixed(1)}×</Text>
                </Group>
                <Text size="xs" c="dimmed">Controls speaking rate sent to the backend TTS engine. 1.0 = normal, 0.8 = slower and clearer, 1.2 = brisker.</Text>
                <Slider value={rate} onChange={handleRate} min={0.5} max={2} step={0.1} marks={RATE_MARKS} color="blue" mb="sm" />
              </Stack>

              <Stack gap="xs">
                <Group justify="space-between">
                  <Text size="sm" fw={500}>Pitch</Text>
                  <Text size="xs" c="dimmed">{pitch.toFixed(1)}</Text>
                </Group>
                <Text size="xs" c="dimmed">Controls voice pitch on the backend. 0.9 sounds warmer. Applies to edge-tts (Neural) voices.</Text>
                <Slider value={pitch} onChange={handlePitch} min={0.5} max={2} step={0.1} marks={PITCH_MARKS} color="violet" mb="sm" />
              </Stack>

              <Stack gap="xs">
                <Group justify="space-between">
                  <Text size="sm" fw={500}>Volume</Text>
                  <Text size="xs" c="dimmed">{volume.toFixed(1)}</Text>
                </Group>
                <Text size="xs" c="dimmed">Playback volume for browser audio output.</Text>
                <Slider value={volume} onChange={handleVolume} min={0.1} max={1} step={0.1} marks={VOLUME_MARKS} color="teal" mb="sm" />
              </Stack>

              <Divider label="Behaviour" labelPosition="left" mt="xs" />

              <Switch
                label="Ignore Background Noise"
                description="Raises the mic sensitivity threshold so ambient noise (fans, AC, office chatter) doesn't accidentally trigger the bot."
                size="sm"
                checked={ignoreBackgroundNoise}
                onChange={(e) => { setSavedName(null); setIgnoreBackgroundNoise(e.currentTarget.checked); }}
              />

              <Switch
                label="Interrupt & Talk"
                description="Bot stops speaking automatically when it detects you talking — no tap needed (barge-in)."
                size="sm"
                checked={allowInterrupt}
                onChange={(e) => { setSavedName(null); setAllowInterrupt(e.currentTarget.checked); }}
              />

            </Stack>
          </Paper>

          {/* Recording Settings */}
          <Paper p="md" withBorder>
            <Stack gap="md">
              <Group justify="space-between" align="flex-start">
                <div>
                  <Text fw={600} size="sm">Recording Settings</Text>
                  <Text size="xs" c="dimmed">Controls mic timing, noise thresholds, and barge-in sensitivity. "Use in Production" pushes these values + Ignore Background Noise + Interrupt &amp; Talk live to the chatbot.</Text>
                </div>
                <Group gap="xs">
                  <Tooltip label="VAPI-style preset: 1-frame barge-in (instant interrupt), barge-in margin 6 (very sensitive), 1s silence timeout, short calibration. Click Use in Production to apply." withArrow position="left" multiline maw={270}>
                    <Button
                      size="xs"
                      variant="light"
                      color="indigo"
                      onClick={applyVapiPreset}
                    >
                      VAPI Defaults
                    </Button>
                  </Tooltip>
                  <Button
                    size="xs"
                    variant="light"
                    loading={savingRecording}
                    leftSection={savedRecording ? <IconCheck size={13} /> : undefined}
                    color={savedRecording ? "teal" : "gray"}
                    onClick={handleSaveRecordingConfig}
                  >
                    {savedRecording ? "Saved" : "Save Draft"}
                  </Button>
                  {recordingConfigId && (
                    <Tooltip label="Delete the voice_recording_config production record" withArrow>
                      <ActionIcon
                        size="sm" variant="light" color="red"
                        loading={deletingProdKey === "recording"}
                        onClick={() => handleDeleteProdRecord("recording")}
                      >
                        <IconTrash size={13} />
                      </ActionIcon>
                    </Tooltip>
                  )}
                  <Button
                    size="xs"
                    variant={recordingMatchProduction ? "light" : "filled"}
                    color={recordingMatchProduction ? "teal" : "green"}
                    loading={promotingRecording}
                    leftSection={recordingMatchProduction ? <IconCheck size={13} /> : <IconRocket size={13} />}
                    onClick={handleUseRecordingInProduction}
                  >
                    {recordingMatchProduction ? "In Production" : "Use in Production"}
                  </Button>
                </Group>
              </Group>

              <Divider label="Timing (milliseconds)" labelPosition="left" />

              <Group grow gap="md">
                <NumberInput
                  label="Min Record Duration"
                  description="Recording won't stop before this (ms)"
                  value={minRecordMs}
                  onChange={(v) => setMinRecordMs(Number(v))}
                  min={500} max={5000} step={100}
                  suffix=" ms"
                  size="sm"
                />
                <NumberInput
                  label="Silence Timeout"
                  description="Auto-send after this much silence (ms)"
                  value={silenceDuration}
                  onChange={(v) => setSilenceDuration(Number(v))}
                  min={500} max={10000} step={100}
                  suffix=" ms"
                  size="sm"
                />
              </Group>

              <Group grow gap="md">
                <NumberInput
                  label="Max Record Duration"
                  description="Hard cap — recording stops at this limit (ms)"
                  value={maxRecordMs}
                  onChange={(v) => setMaxRecordMs(Number(v))}
                  min={5000} max={120000} step={1000}
                  suffix=" ms"
                  size="sm"
                />
                <NumberInput
                  label="Noise Calibration Time"
                  description="First N ms used to measure room noise floor (ms)"
                  value={calibrationMs}
                  onChange={(v) => setCalibrationMs(Number(v))}
                  min={200} max={3000} step={100}
                  suffix=" ms"
                  size="sm"
                />
              </Group>

              <Divider label="Noise Detection" labelPosition="left" />

              <Group grow gap="md">
                <Stack gap="xs">
                  <Group justify="space-between">
                    <Text size="sm" fw={500}>Noise Margin (Normal)</Text>
                    <Text size="xs" c="dimmed">{noiseMarginDefault}</Text>
                  </Group>
                  <Text size="xs" c="dimmed">Threshold = noise floor + this margin. Higher = less sensitive.</Text>
                  <Slider
                    value={noiseMarginDefault}
                    onChange={setNoiseMarginDefault}
                    min={4} max={40} step={1}
                    marks={[{ value: 12, label: "12 (default)" }]}
                    color="orange"
                    mb="sm"
                  />
                </Stack>
                <Stack gap="xs">
                  <Group justify="space-between">
                    <Text size="sm" fw={500}>Noise Margin (High — Ignore Background)</Text>
                    <Text size="xs" c="dimmed">{noiseMarginHigh}</Text>
                  </Group>
                  <Text size="xs" c="dimmed">Used when "Ignore Background Noise" is ON. Set higher than Normal.</Text>
                  <Slider
                    value={noiseMarginHigh}
                    onChange={setNoiseMarginHigh}
                    min={10} max={60} step={1}
                    marks={[{ value: 28, label: "28 (default)" }]}
                    color="red"
                    mb="sm"
                  />
                </Stack>
              </Group>

              <Divider label="Barge-in & Level" labelPosition="left" />

              <Group grow gap="md">
                <NumberInput
                  label="Barge-in Frames"
                  description="Consecutive loud frames before interrupting TTS. 1 = instant, 3 = safer."
                  value={bargeInFrames}
                  onChange={(v) => setBargeInFrames(Number(v))}
                  min={1} max={20} step={1}
                  suffix=" frames"
                  size="sm"
                />
                <NumberInput
                  label="Level Update Rate"
                  description="Audio level UI updates per second"
                  value={levelFps}
                  onChange={(v) => setLevelFps(Number(v))}
                  min={5} max={60} step={1}
                  suffix=" fps"
                  size="sm"
                />
              </Group>

              <Group grow gap="md">
                <Stack gap="xs">
                  <Group justify="space-between">
                    <Text size="sm" fw={500}>Barge-in Speech Margin</Text>
                    <Text size="xs" c="dimmed">{bargeInNoiseMargin}</Text>
                  </Group>
                  <Text size="xs" c="dimmed">
                    Sensitivity of interruption detection during TTS. Lower = easier to interrupt (set low for voice-first UX). Separate from listening-phase margin.
                  </Text>
                  <Slider
                    value={bargeInNoiseMargin}
                    onChange={setBargeInNoiseMargin}
                    min={2} max={30} step={1}
                    marks={[{ value: 6, label: "6 (VAPI)" }, { value: 10, label: "10 (default)" }, { value: 20, label: "20 (strict)" }]}
                    color="cyan"
                    mb="sm"
                  />
                </Stack>
              </Group>

              <Divider label="Response Speed" labelPosition="left" />

              <Switch
                label="Disable Web Crawl in Voice Mode"
                description="Skips live web search during voice calls. Saves 3–10 s per response. Bot answers from knowledge base only — recommended for voice UX."
                size="sm"
                checked={disableCrawl}
                onChange={(e) => setDisableCrawl(e.currentTarget.checked)}
              />

              <Group grow gap="md">
                <NumberInput
                  label="Max Spoken Sentences"
                  description="Limits how many sentences the bot speaks aloud. 1 = fastest first reply, 3 = fuller answer."
                  value={maxSpokenSentences}
                  onChange={(v) => setMaxSpokenSentences(Number(v))}
                  min={1} max={6} step={1}
                  suffix=" sentences"
                  size="sm"
                />
                <NumberInput
                  label="Max Spoken Characters"
                  description="Hard character cap on spoken text. Shorter = faster TTS. 200 ≈ 2 sentences."
                  value={maxSpokenChars}
                  onChange={(v) => setMaxSpokenChars(Number(v))}
                  min={80} max={600} step={10}
                  suffix=" chars"
                  size="sm"
                />
              </Group>

              <Select
                label="Voice Language Model"
                description="Gemini model used for chat responses during voice calls. flash-lite is fastest; flash is more capable."
                value={voiceLanguageModel}
                onChange={(v) => setVoiceLanguageModel(v ?? "gemini-2.0-flash-lite")}
                data={[
                  { value: "gemini-2.0-flash-lite", label: "gemini-2.0-flash-lite (fastest, default)" },
                  { value: "gemini-2.0-flash",      label: "gemini-2.0-flash (fast + smarter)" },
                  { value: "gemini-1.5-flash",      label: "gemini-1.5-flash (balanced)" },
                  { value: "gemini-1.5-pro",        label: "gemini-1.5-pro (slowest, most capable)" },
                ]}
                size="sm"
              />
            </Stack>
          </Paper>

          {/* Voice prompt name */}
          <Paper p="md" withBorder>
            <Stack gap="sm">
              <Text fw={600} size="sm">Voice Prompt Name</Text>
              <TextInput
                label="Name"
                description="Identifies this voice prompt in the saved list above"
                placeholder="e.g. Karma — British Male v2"
                size="sm"
                {...form.getInputProps("promptName")}
                onChange={(e) => { setSavedName(null); form.getInputProps("promptName").onChange(e); }}
              />
            </Stack>
          </Paper>

          {/* Prompt text editor */}
          <Paper p="md" withBorder>
            <Stack gap="xs">
              <Group justify="space-between" align="center">
                <div>
                  <Text fw={600} size="sm">Prompt Text</Text>
                  <Text size="xs" c="dimmed">
                    Edit freely. The <b>## TTS Voice Settings</b> block stays in sync with the sliders above.
                  </Text>
                </div>
                <Tooltip label="Rebuild from all fields above" position="left">
                  <Button size="xs" variant="light" color="gray" leftSection={<IconRefresh size={13} />} onClick={handleRegenerate}>
                    Regenerate
                  </Button>
                </Tooltip>
              </Group>
              <Textarea
                value={promptText}
                onChange={handlePromptTextChange}
                minRows={16}
                autosize
                size="xs"
                styles={{ input: { fontFamily: "monospace" } }}
              />
            </Stack>
          </Paper>

          {error && <Alert color="red" variant="light" title="Error">{error}</Alert>}

          <Divider />

          <Group justify="flex-end">
            <Button type="submit" size="sm" loading={loading} leftSection={<IconCheck size={14} />}>
              Save Voice Prompt
            </Button>
          </Group>
        </Stack>
      </form>

      {/* ── Voice Commands ─────────────────────────────────────────────── */}
      <VoiceCommandsSection
        configId={voiceConfig.voiceCommandsConfigId}
        initialData={voiceConfig.voiceCommandsData}
      />
    </Stack>
  );
}

// ── Voice Commands Section ────────────────────────────────────────────────────

function VoiceCommandsSection({
  configId,
  initialData,
}: {
  configId: string | null;
  initialData: { commands: VoiceCommandGroup[] };
}) {
  const [commands,   setCommands]   = useState<VoiceCommandGroup[]>(
    initialData?.commands?.length ? initialData.commands : []
  );
  const [newPhrases, setNewPhrases] = useState<Record<number, string>>({});
  const [loading,    setLoading]    = useState(false);
  const [error,      setError]      = useState<string | null>(null);
  const [saved,      setSaved]      = useState(false);

  // Keep a ref to configId so the save handler is always fresh
  const configIdRef = useRef<string | null>(configId);
  useEffect(() => { configIdRef.current = configId; }, [configId]);

  const addCommand = () =>
    setCommands((prev) => [...prev, { command: "", phrases: [], response: "" }]);

  const removeCommand = (i: number) =>
    setCommands((prev) => prev.filter((_, idx) => idx !== i));

  const updateCmd = (i: number, field: keyof VoiceCommandGroup, value: string) =>
    setCommands((prev) =>
      prev.map((c, idx) => (idx === i ? { ...c, [field]: value } : c))
    );

  const addPhrase = (ci: number) => {
    const phrase = (newPhrases[ci] || "").trim().toLowerCase();
    if (!phrase) return;
    setCommands((prev) =>
      prev.map((c, idx) =>
        idx === ci ? { ...c, phrases: [...c.phrases, phrase] } : c
      )
    );
    setNewPhrases((prev) => ({ ...prev, [ci]: "" }));
  };

  const removePhrase = (ci: number, pi: number) =>
    setCommands((prev) =>
      prev.map((c, idx) =>
        idx === ci ? { ...c, phrases: c.phrases.filter((_, pidx) => pidx !== pi) } : c
      )
    );

  const handleSave = async () => {
    setLoading(true);
    setError(null);
    setSaved(false);
    try {
      const payload = JSON.stringify({ commands });
      const id = configIdRef.current;
      if (id) {
        await updatePrompt(id, {
          prompt_name:  "Voice Commands",
          prompt_key:   "voice_commands",
          prompt_value: payload,
        });
      } else {
        await createPrompt({
          prompt_name:  "Voice Commands",
          prompt_key:   "voice_commands",
          prompt_value: payload,
        });
      }
      setSaved(true);
      setTimeout(() => setSaved(false), 3000);
    } catch (e) {
      setError(String(e));
    } finally {
      setLoading(false);
    }
  };

  return (
    <Paper p="md" withBorder>
      <Stack gap="md">
        {/* Header */}
        <Group justify="space-between" align="flex-start">
          <div>
            <Group gap="xs" align="center">
              <ThemeIcon size="sm" variant="light" color="violet">
                <IconMicrophone size={13} />
              </ThemeIcon>
              <Text fw={700} size="sm">Voice Commands</Text>
            </Group>
            <Text size="xs" c="dimmed" mt={2}>
              Phrases users speak to trigger navigation (e.g. "start fresh", "continue").
              Changes here take effect on the next agent restart — no code changes needed.
            </Text>
          </div>
          <Button
            size="xs"
            variant="light"
            color="violet"
            leftSection={<IconPlus size={13} />}
            onClick={addCommand}
          >
            Add Command
          </Button>
        </Group>

        {commands.length === 0 && (
          <Text size="sm" c="dimmed" ta="center" py="md">
            No voice commands yet. Click "Add Command" to create one.
          </Text>
        )}

        {commands.map((cmd, ci) => (
          <Paper key={ci} p="sm" withBorder style={{ background: "var(--mantine-color-default-hover)" }}>
            <Stack gap="sm">
              <Group justify="space-between" align="center">
                <Badge variant="dot" color="violet" size="sm">
                  Command {ci + 1}
                </Badge>
                <ActionIcon
                  size="sm"
                  color="red"
                  variant="subtle"
                  onClick={() => removeCommand(ci)}
                  title="Delete this command group"
                >
                  <IconTrash size={13} />
                </ActionIcon>
              </Group>

              <TextInput
                label="Command Name"
                description="Internal key sent to frontend (e.g. start_fresh, continue)"
                placeholder="start_fresh"
                size="sm"
                value={cmd.command}
                onChange={(e) => updateCmd(ci, "command", e.currentTarget.value)}
              />

              <Textarea
                label="Agent Response"
                description="What the agent says when this command is triggered"
                placeholder="Sure! Starting fresh. How can I help you today?"
                size="sm"
                minRows={2}
                autosize
                value={cmd.response}
                onChange={(e) => updateCmd(ci, "response", e.currentTarget.value)}
              />

              <div>
                <Text size="xs" fw={500} mb={4}>Trigger Phrases</Text>
                <Text size="xs" c="dimmed" mb={6}>
                  User saying any of these (exact match, case-insensitive) triggers this command
                </Text>
                <Stack gap={6}>
                  {cmd.phrases.map((phrase, pi) => (
                    <Group key={pi} gap="xs" wrap="nowrap">
                      <Badge
                        variant="light"
                        color="blue"
                        size="md"
                        style={{ flex: 1, textTransform: "none", fontWeight: 400, justifyContent: "flex-start" }}
                      >
                        {phrase}
                      </Badge>
                      <ActionIcon
                        size="xs"
                        color="red"
                        variant="subtle"
                        onClick={() => removePhrase(ci, pi)}
                      >
                        <IconTrash size={11} />
                      </ActionIcon>
                    </Group>
                  ))}

                  {/* Add phrase row */}
                  <Group gap="xs" wrap="nowrap">
                    <TextInput
                      placeholder='e.g. "start fresh" — press Enter to add'
                      size="xs"
                      style={{ flex: 1 }}
                      value={newPhrases[ci] || ""}
                      onChange={(e) =>
                        setNewPhrases((prev) => ({ ...prev, [ci]: e.currentTarget.value }))
                      }
                      onKeyDown={(e) => {
                        if (e.key === "Enter") { e.preventDefault(); addPhrase(ci); }
                      }}
                    />
                    <ActionIcon
                      size="sm"
                      variant="light"
                      color="blue"
                      onClick={() => addPhrase(ci)}
                    >
                      <IconPlus size={13} />
                    </ActionIcon>
                  </Group>
                </Stack>
              </div>
            </Stack>
          </Paper>
        ))}

        {error && <Alert color="red" variant="light" title="Error">{error}</Alert>}
        {saved && (
          <Alert color="green" variant="light" title="Saved" icon={<IconCheck size={14} />}>
            Voice commands saved. Restart the agent worker to apply changes.
          </Alert>
        )}

        <Divider />
        <Group justify="flex-end">
          <Button
            size="sm"
            loading={loading}
            leftSection={<IconCheck size={14} />}
            onClick={handleSave}
            disabled={commands.length === 0}
          >
            Save Voice Commands
          </Button>
        </Group>
      </Stack>
    </Paper>
  );
}
