// agent.jsx — Vapi.ai voice agent integration.
// Replaces the former Web Speech API + OpenAI TTS + n8n chat webhook approach.
// Vapi handles STT, LLM, and TTS entirely. This file manages call state and
// data capture (via save_section tool calls intercepted from Vapi message events).

const { useState: useState_a, useEffect: useEffect_a, useRef: useRef_a,
        useCallback: useCallback_a, useMemo: useMemo_a } = React;

const CFG = () => window.CFM_CONFIG || {};

// ─── Value coercion (kept for applying save_section data to checkbox fields) ──
function normalize(str) {
  return (str || "").toString().toLowerCase().replace(/[^\w\s-]/g, " ").replace(/\s+/g, " ").trim();
}
function scoreOption(raw, optionText) {
  const t = normalize(raw), o = normalize(optionText);
  if (!t || !o) return 0;
  if (t === o) return 200;
  if (t.includes(o) || o.includes(t)) return 100;
  const tWords = new Set(t.split(" "));
  const oWords = o.split(" ").filter((w) => w.length >= 3);
  let score = 0;
  for (const w of oWords) if (tWords.has(w)) score += 2;
  if (oWords[0] && tWords.has(oWords[0])) score += 1;
  return score;
}
function canonicalOption(raw, options) {
  let best = null, bestScore = 0;
  for (const opt of options) {
    const txt = typeof opt === "string" ? opt : opt.label;
    const s = scoreOption(raw, txt);
    if (s > bestScore) { best = txt; bestScore = s; }
  }
  return bestScore >= 2 ? best : null;
}
function coerceValue(field, raw) {
  if (raw == null) return null;
  const optLabels = (field.options || []).map((o) => (typeof o === "string" ? o : o.label));
  switch (field.type) {
    case "radio": {
      // Try fuzzy match first; if raw is already a valid option, use it
      const match = canonicalOption(raw, optLabels);
      return match || (typeof raw === "string" ? raw.trim() : null);
    }
    case "checkbox": {
      const arr = Array.isArray(raw) ? raw : String(raw).split(/[,\n]/);
      const out = [];
      for (const r of arr) {
        const c = canonicalOption(r.trim(), optLabels);
        if (c && !out.includes(c)) out.push(c);
        else if (!c && r.trim() && !out.includes(r.trim())) out.push(r.trim());
      }
      return out.length ? out : null;
    }
    case "limited-checkbox": {
      const max = field.max || 3;
      const arr = Array.isArray(raw) ? raw : String(raw).split(/[,\n]/);
      const out = [];
      for (const r of arr) {
        const c = canonicalOption(r.trim(), optLabels);
        if (c && !out.includes(c)) out.push(c);
        else if (!c && r.trim() && !out.includes(r.trim())) out.push(r.trim());
      }
      return out.length ? out.slice(0, max) : null;
    }
    case "file":
      return null; // files uploaded manually in review screen
    default:
      return raw != null ? String(raw).replace(/^[\s"']+|[\s"']+$/g, "") : null;
  }
}

function applyUpdates(sections, updates, onUpdate) {
  const justSet = {};
  if (!updates || typeof updates !== "object") return justSet;
  for (const [fieldId, raw] of Object.entries(updates)) {
    const field = window.fieldById(sections, fieldId);
    if (!field) continue;
    const val = coerceValue(field, raw);
    if (val == null || (Array.isArray(val) && val.length === 0)) continue;
    onUpdate(fieldId, val);
    justSet[fieldId] = val;
  }
  return justSet;
}

// ─── The Vapi agent hook ──────────────────────────────────────────────────────
function useVapiAgent({ sections, onUpdate, onComplete, crawlContext, crawlData, websiteUrl }) {
  const cfg = CFG();

  // Call state
  const [callStatus, setCallStatus] = useState_a("idle"); // idle | connecting | active | ending | ended | error
  const [transcript,  setTranscript]  = useState_a([]);   // [{role, text, ts}]
  const [volumeLevel, setVolumeLevel] = useState_a(0);    // 0–1 from Vapi volume events
  const [isMuted,     setIsMuted]     = useState_a(false);
  const [agentSpeaking, setAgentSpeaking] = useState_a(false);
  const [userSpeaking,  setUserSpeaking]  = useState_a(false);
  const [currentSection, setCurrentSection] = useState_a(null); // section_name from last save_section
  const [sectionsCompleted, setSectionsCompleted] = useState_a([]);
  const [justSet,     setJustSet]     = useState_a({});
  const [error,       setError]       = useState_a(null);
  const [callDuration, setCallDuration] = useState_a(0); // seconds

  const vapiRef    = useRef_a(null);  // Vapi SDK instance
  const timerRef   = useRef_a(null);  // call duration interval
  const startedRef = useRef_a(false); // guard against double-start

  // ── Duration timer ───────────────────────────────────────────────────────
  const startTimer = useCallback_a(() => {
    if (timerRef.current) return;
    setCallDuration(0);
    timerRef.current = setInterval(() => setCallDuration((d) => d + 1), 1000);
  }, []);
  const stopTimer = useCallback_a(() => {
    if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
  }, []);
  useEffect_a(() => () => stopTimer(), [stopTimer]);

  // ── Vapi message handler ─────────────────────────────────────────────────
  const handleVapiMessage = useCallback_a((msg) => {
    if (!msg || !msg.type) return;

    if (msg.type === "transcript" && msg.transcriptType === "final") {
      const role = msg.role === "assistant" ? "agent" : "user";
      setTranscript((t) => [...t, { role, text: msg.transcript, ts: Date.now() }]);
    }

    if (msg.type === "speech-update") {
      if (msg.role === "assistant") setAgentSpeaking(msg.status === "started");
      if (msg.role === "user")      setUserSpeaking(msg.status === "started");
    }

    // Log all messages in dev so we can diagnose what Vapi actually sends
    if (process.env && process.env.NODE_ENV !== "production") {
      console.log("[Vapi msg]", msg.type, msg);
    } else {
      console.log("[Vapi msg]", msg.type);
    }

    if (msg.type === "tool-calls") {
      // Vapi uses "toolCallList" in some SDK versions and "toolCalls" in others
      const list = msg.toolCallList || msg.toolCalls || [];
      for (const tc of list) {
        const fn = tc.function || {};
        if (fn.name !== "save_section") continue;
        let args = {};
        try { args = typeof fn.arguments === "string" ? JSON.parse(fn.arguments) : fn.arguments; } catch (e) {}
        if (!args.data) continue;

        // 1. Send result back to Vapi immediately so the conversation continues
        if (vapiRef.current) {
          vapiRef.current.send({
            type: "add-message",
            message: {
              role: "tool",
              toolCallId: tc.id,
              content: "Section saved successfully.",
            },
          });
        }

        // 2. Update form fields in real-time
        const changed = applyUpdates(sections, args.data, onUpdate);
        if (Object.keys(changed).length) {
          setJustSet(changed);
          setTimeout(() => setJustSet({}), 800);
        }

        // 3. Track section completion
        if (args.section_name) {
          setCurrentSection(args.section_name);
          setSectionsCompleted((prev) =>
            prev.includes(args.section_name) ? prev : [...prev, args.section_name]
          );
        }

        // 4. Forward to n8n via server route (fire-and-forget; works when deployed)
        fetch(window.location.origin + "/vapi/save-section", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            message: {
              toolCallList: [tc],
              call: { id: msg.call && msg.call.id },
            },
          }),
        }).catch(() => {});
      }
    }

    if (msg.type === "end-of-call-report") {
      // Structured data from analysisPlan — apply as a backup fill
      const structured = msg.analysis && msg.analysis.structuredData;
      if (structured && typeof structured === "object") {
        for (const sectionData of Object.values(structured)) {
          if (sectionData && typeof sectionData === "object") {
            applyUpdates(sections, sectionData, onUpdate);
          }
        }
      }
    }
  }, [sections, onUpdate]);

  // ── Start call ────────────────────────────────────────────────────────────
  const startCall = useCallback_a(async () => {
    // Allow retry from error state; block if already starting/active/ended
    if (startedRef.current || (callStatus !== "idle" && callStatus !== "error")) return;

    // Reset error state before each attempt
    setError(null);
    setCallStatus("idle");

    // Use pre-loaded window.Vapi (set by the module script in index.html),
    // or dynamic-import as a fallback in case the module hasn't resolved yet.
    let VapiSDK = window.Vapi;
    if (!VapiSDK) {
      try {
        const m = await import("https://esm.sh/@vapi-ai/web");
        VapiSDK = m.default;
        window.Vapi = VapiSDK;
      } catch (e) {
        setError("Vapi SDK failed to load. Check your internet connection and reload.");
        setCallStatus("error");
        return;
      }
    }
    const publicKey = cfg.VAPI_PUBLIC_KEY;
    if (!publicKey) {
      setError("VAPI_PUBLIC_KEY is not configured. Add it to config.js.");
      setCallStatus("error");
      return;
    }
    startedRef.current = true;
    setCallStatus("connecting");
    setError(null);
    setTranscript([]);
    setSectionsCompleted([]);
    setCurrentSection(null);

    try {
      const vapi = new VapiSDK(publicKey);
      vapiRef.current = vapi;

      vapi.on("call-start",   () => { setCallStatus("active"); startTimer(); });
      vapi.on("call-end",     () => {
        setCallStatus("ended");
        stopTimer();
        startedRef.current = false;
        onComplete && onComplete();
      });
      vapi.on("volume-level", (v) => setVolumeLevel(v));
      vapi.on("message",      handleVapiMessage);
      vapi.on("error", (err) => {
        // "Meeting has ended" is Daily.co's normal ejection signal when Vapi
        // hangs up cleanly. The call-end event fires alongside it — let that
        // handle the state transition instead of showing a red error.
        const errorMsg = err && err.error && (err.error.errorMsg || (err.error.error && err.error.error.msg) || "");
        if (err && err.type === "daily-error" && errorMsg === "Meeting has ended") {
          console.info("Vapi call ended (normal Daily.co ejection)");
          return;
        }

        console.error("Vapi error:", err);
        // Always produce a string — err.error.message can itself be an object
        let msg = "Voice call error — please try again.";
        try {
          if (typeof err === "string") msg = err;
          else if (err) {
            const e = err.error;
            if (typeof err.message === "string" && err.message) msg = err.message;
            else if (e && typeof e.errorMsg === "string" && e.errorMsg) msg = e.errorMsg;
            else if (e && typeof e.message === "string" && e.message) msg = e.message;
            else if (err.statusCode) msg = `Vapi error ${err.statusCode} — check config.js.`;
            else msg = JSON.stringify(err);
          }
        } catch (_) {}
        setError(msg);
        setCallStatus("error");
        stopTimer();
        startedRef.current = false;
      });

      // Build transient assistant config (no pre-configured assistant ID required)
      const systemPrompt = window.buildVapiSystemPrompt(
        websiteUrl || "",
        crawlContext || null,
        crawlData ? window.buildPrefilledSummary(crawlData) : ""
      );
      const agentName = (cfg.brand || {}).agentName || "Momentum";

      // Always include save_section as a client-side tool (no server.url).
      // The browser tool-calls event fires → applyUpdates fills the review form.
      // The handler also forwards to n8n via fetch for backend persistence.
      const tools = [window.buildVapiSaveSectionTool()];

      const voiceConfig = buildVoiceConfig(cfg);

      const assistantConfig = {
        name:               agentName,
        firstMessage:       window.VAPI_FIRST_MESSAGE,
        firstMessageMode:   "assistant-speaks-first",
        transcriber: {
          provider:    "deepgram",
          model:       "nova-2",
          language:    "en-US",
          endpointing: 500,  // Vapi max is 500ms
        },
        // Top-level tools: Vapi-managed. Without server.url, Vapi fires the
        // tool-calls WebSocket event to the browser so we can handle it client-side.
        // DO NOT put save_section in model.tools — the LLM would generate function
        // calls but Vapi would have no route for them and silently drop them.
        tools,
        model: {
          provider: "openai",
          model:    "gpt-4o",
          messages: [{ role: "system", content: systemPrompt }],
        },
        voice: voiceConfig,
        maxDurationSeconds: 2700,
        endCallMessage:
          "Thank you so much — this was incredibly helpful. Our design team will review " +
          "everything and your account manager will be in touch within one business day. " +
          "Have a wonderful day!",
        endCallPhrases: [
          "we'll be in touch soon",
          "have a wonderful day",
          "talk soon",
          "our design team will be in touch",
        ],
        analysisPlan: window.VAPI_ANALYSIS_PLAN,
      };

      // Start using assistant ID if configured (uses pre-built assistant from Vapi dashboard)
      // Otherwise, use the transient config built above.
      if (cfg.VAPI_ASSISTANT_ID) {
        vapi.start({
          assistantId: cfg.VAPI_ASSISTANT_ID,
          assistantOverrides: {
            variableValues: {
              website_url:       websiteUrl || "",
              crawl_context:     JSON.stringify(crawlContext || {}),
              prefilled_summary: crawlData ? window.buildPrefilledSummary(crawlData) : "",
            },
          },
        });
      } else {
        vapi.start(assistantConfig);
      }
    } catch (err) {
      setError(err.message || "Failed to start call.");
      setCallStatus("error");
      startedRef.current = false;
    }
  }, [callStatus, cfg, websiteUrl, crawlContext, crawlData, handleVapiMessage, startTimer, stopTimer, onComplete]);

  // ── End call ──────────────────────────────────────────────────────────────
  const endCall = useCallback_a(() => {
    if (vapiRef.current) {
      setCallStatus("ending");
      try { vapiRef.current.stop(); } catch (e) {}
    }
  }, []);

  // ── Mute toggle ───────────────────────────────────────────────────────────
  const toggleMute = useCallback_a(() => {
    setIsMuted((m) => {
      const next = !m;
      try { if (vapiRef.current) vapiRef.current.setMuted(next); } catch (e) {}
      return next;
    });
  }, []);

  // ── Cleanup on unmount ────────────────────────────────────────────────────
  useEffect_a(() => () => {
    stopTimer();
    if (vapiRef.current) { try { vapiRef.current.stop(); } catch (e) {} }
  }, [stopTimer]);

  const totalSections = 17;
  const pct = Math.round((sectionsCompleted.length / totalSections) * 100);

  return {
    callStatus, transcript, volumeLevel, isMuted,
    agentSpeaking, userSpeaking, currentSection,
    sectionsCompleted, justSet, error, callDuration, pct,
    controls: { startCall, endCall, toggleMute },
  };
}

// ─── Build voice config from CFM_CONFIG ───────────────────────────────────────
function buildVoiceConfig(cfg) {
  const vCfg = cfg.voice || {};
  const provider = vCfg.provider || "deepgram";
  // Vapi uses short Deepgram voice IDs — no "aura-" prefix, no "-en" suffix
  const defaultVoiceId = provider === "11labs" ? "pFZP5JQG7iQjIQuC4Bku" : "asteria";
  return {
    provider: provider,
    voiceId:  vCfg.voiceId || defaultVoiceId,
    ...(vCfg.stability    != null ? { stability:    vCfg.stability }    : {}),
    ...(vCfg.similarityBoost != null ? { similarityBoost: vCfg.similarityBoost } : {}),
  };
}

// ─── Orb state derived from Vapi call state ───────────────────────────────────
function orbStateFromCall(callStatus, agentSpeaking, userSpeaking) {
  if (callStatus === "connecting")  return "thinking";
  if (callStatus === "ending")      return "thinking";
  if (agentSpeaking)                return "speaking";
  if (userSpeaking)                 return "listening";
  if (callStatus === "active")      return "idle";
  return "idle";
}

// ─── Format seconds → M:SS ────────────────────────────────────────────────────
function formatDuration(secs) {
  const m = Math.floor(secs / 60);
  const s = secs % 60;
  return m + ":" + String(s).padStart(2, "0");
}

// ─── Section name → human label ───────────────────────────────────────────────
const SECTION_LABELS = {
  basic_info:          "Basic Information",
  heart_of_business:   "Heart of Your Business",
  problem_you_solve:   "The Problem You Solve",
  life_before_after:   "Life Before & After",
  your_difference:     "Your Difference",
  ideal_customer:      "Ideal Customer",
  identity:            "Identity",
  industry_perspective:"Industry Perspective",
  brand_personality:   "Brand Personality",
  language_messaging:  "Language & Messaging",
  brand_style:         "Brand Style",
  design_branding:     "Design & Branding",
  reference_sites:     "Reference Websites",
  content_functions:   "Content & Functions",
  dev_logistics:       "Development & Logistics",
  public_info:         "Public Info",
  legal:               "Legal",
};

// ─── Vapi Call Widget ─────────────────────────────────────────────────────────
function VapiCallWidget({ agent, onEndCall }) {
  const {
    callStatus, transcript, volumeLevel, isMuted,
    agentSpeaking, userSpeaking, currentSection,
    sectionsCompleted, error, callDuration, pct,
    controls,
  } = agent;

  const scrollRef = useRef_a(null);
  const cfg = CFG();
  const agentName = (cfg.brand || {}).agentName || "Momentum";

  useEffect_a(() => {
    if (scrollRef.current)
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  }, [transcript]);

  const orbState = orbStateFromCall(callStatus, agentSpeaking, userSpeaking);

  const statusLabel = {
    idle:       "Ready",
    connecting: "Connecting…",
    active:     agentSpeaking ? "Speaking" : userSpeaking ? "Listening" : "Connected",
    ending:     "Ending call…",
    ended:      "Call ended",
    error:      "Error",
  }[callStatus] || "—";

  const isLive = callStatus === "active" || callStatus === "ending";

  return (
    <div className="vapi-widget">
      {/* Header */}
      <div className="vapi-widget-head">
        <div className="vapi-orb-wrap" style={{ "--volume": volumeLevel }}>
          <Orb size={56} state={orbState} />
        </div>
        <div className="vapi-meta">
          <div className="vapi-name"><span className="liveball"></span>{agentName}</div>
          <div className={`vapi-status ${orbState}`}>
            {isLive && (agentSpeaking || userSpeaking) ? <Wave /> : null} {statusLabel}
          </div>
        </div>
        {isLive && (
          <div className="vapi-duration">{formatDuration(callDuration)}</div>
        )}
      </div>

      {/* Progress bar */}
      {isLive && (
        <div className="vapi-progress-row">
          <div className="vapi-progress-label">
            {sectionsCompleted.length} / 17 sections
            {currentSection && ` · ${SECTION_LABELS[currentSection] || currentSection}`}
          </div>
          <div className="vapi-progress-track">
            <div className="vapi-progress-fill" style={{ width: pct + "%" }} />
          </div>
        </div>
      )}

      {/* Transcript */}
      <div className="vapi-transcript" ref={scrollRef}>
        {transcript.length === 0 && callStatus === "connecting" && (
          <div className="vapi-transcript-placeholder">Connecting to {agentName}…</div>
        )}
        {transcript.length === 0 && callStatus === "active" && (
          <div className="vapi-transcript-placeholder">Listening…</div>
        )}
        {transcript.map((m, i) => (
          <div key={i} className={`vapi-bubble ${m.role}`}>{m.text}</div>
        ))}
      </div>

      {/* Error */}
      {error && <div className="vapi-error">{error}</div>}

      {/* Controls */}
      <div className="vapi-controls">
        {isLive && (
          <button
            className={`vapi-btn icon ${isMuted ? "muted" : ""}`}
            onClick={controls.toggleMute}
            aria-label={isMuted ? "Unmute mic" : "Mute mic"}
            title={isMuted ? "Mic muted" : "Mic on"}
          >
            {isMuted ? <Icon.MicOff /> : <Icon.Mic />}
          </button>
        )}
        {callStatus === "idle" && (
          <button className="vapi-btn primary" onClick={controls.startCall}>
            Start call <Icon.Arrow />
          </button>
        )}
        {callStatus === "error" && (
          <>
            <button className="vapi-btn primary" onClick={controls.startCall}>
              Try again <Icon.Arrow />
            </button>
            <button className="vapi-btn" onClick={onEndCall}>
              View answers anyway
            </button>
          </>
        )}
        {isLive && (
          <button className="vapi-btn danger" onClick={() => { controls.endCall(); onEndCall && onEndCall(); }}>
            End call
          </button>
        )}
        {callStatus === "ended" && (
          <button className="vapi-btn primary" onClick={onEndCall}>
            Review answers <Icon.Arrow />
          </button>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { useVapiAgent, VapiCallWidget, applyUpdates, coerceValue });
