/* global React, Icon, Avatar */
const { useState, useEffect, useRef, useMemo } = React;

// ── Gentle curve waveform ────────────────────────────────
// Two layered sine waves, one drifting; amplitude swells when recording.
function VoiceWave({ recording, recorded, analyser }) {
  const [t, setT] = useState(0);
  const levelRef = useRef(0); // smoothed 0..1 live audio level
  const dataRef = useRef(null);
  const wrapRef = useRef(null);
  const [W, setW] = useState(540); // measured element width (drawn 1:1, no stretch)
  const [H, setH] = useState(64); // measured element height

  // Track the real rendered size so the wave is drawn at native dimensions and
  // the stroke/amplitude/centre never distort with the container.
  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const measure = () => {
      setW(Math.max(80, Math.round(el.clientWidth)));
      setH(Math.max(24, Math.round(el.clientHeight)));
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  useEffect(() => {
    let raf;
    const start = performance.now();
    if (analyser && !dataRef.current)
      dataRef.current = new Uint8Array(analyser.frequencyBinCount);
    const tick = (now) => {
      setT((now - start) / 1000);
      if (analyser && dataRef.current) {
        analyser.getByteFrequencyData(dataRef.current);
        let sum = 0;
        const n = Math.floor(dataRef.current.length * 0.66);
        for (let i = 0; i < n; i++) sum += dataRef.current[i];
        const target = Math.min(1, (sum / n / 255) * 1.8);
        levelRef.current += (target - levelRef.current) * 0.25;
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [analyser]);

  const mid = H / 2;
  const maxA = H * 0.42; // amplitude ceiling = ~42% of height each way (fills the slot)

  const live = analyser ? levelRef.current : null;
  // Express amplitudes as a fraction of the available height so it fills any size.
  const baseFrac = recording ? 0.7 : recorded ? 0.62 : 0.28;
  const breath = recording
    ? Math.sin(t * 1.3) * 0.12 + Math.sin(t * 2.7) * 0.06
    : recorded
      ? Math.sin(t * 0.9) * 0.1 + Math.sin(t * 1.9) * 0.05
      : 0;
  const frac = live != null ? 0.25 + live * 0.7 : baseFrac + breath;
  const amp = Math.max(2, Math.min(1, frac) * maxA);

  // Wavelength fixed in PIXELS (not divided across the width), so wider players
  // simply show more wave cycles at the same height/steepness — no flattening.
  const buildPath = (a, driftBase, freqMix) => {
    const pts = [];
    const step = 4;
    const drift = (recording ? t * 1.9 : t * 0.7) + driftBase;
    const k = (Math.PI * 2) / 130; // ~130px per cycle, constant regardless of W
    for (let x = 0; x <= W; x += step) {
      // Gentle edge fade so ends taper into the baseline
      const edge = Math.min(1, Math.min(x, W - x) / 40);
      const y =
        mid +
        edge *
          (Math.sin(x * k + drift) * a +
            Math.sin(x * k * 2.3 + drift * 1.5) * (a * 0.3 * freqMix) +
            Math.sin(x * k * 0.6 - drift * 0.8) * (a * 0.22));
      pts.push([x, y]);
    }
    let d = `M ${pts[0][0]} ${pts[0][1]}`;
    for (let i = 1; i < pts.length; i++) {
      const [px, py] = pts[i - 1];
      const [cx, cy] = pts[i];
      const mx = (px + cx) / 2;
      d += ` Q ${px} ${py} ${mx} ${(py + cy) / 2}`;
    }
    d += ` T ${pts[pts.length - 1][0]} ${pts[pts.length - 1][1]}`;
    return d;
  };

  const path = useMemo(() => buildPath(amp, 0, 1), [t, amp, recording, W, H]);
  const shadowPath = useMemo(
    () => buildPath(amp * 0.62, 0.9, 0.6),
    [t, amp, recording, W, H],
  );

  return (
    <div
      ref={wrapRef}
      className="voice-wave-wrap"
      style={{ width: "100%", height: "100%" }}
    >
      {/* viewBox matches measured px → 1:1, uniform stroke, true vertical centre */}
      <svg
        className="voice-wave-svg"
        viewBox={`0 0 ${W} ${H}`}
        preserveAspectRatio="none"
        width="100%"
        height="100%"
        aria-hidden="true"
      >
        <defs>
          <linearGradient id="vw-grad" x1="0" x2="1" y1="0" y2="0">
            <stop offset="0" stopColor="var(--accent)" stopOpacity="0.15" />
            <stop offset="0.5" stopColor="var(--accent)" stopOpacity="1" />
            <stop offset="1" stopColor="var(--accent)" stopOpacity="0.15" />
          </linearGradient>
          <linearGradient id="vw-shadow" x1="0" x2="1" y1="0" y2="0">
            <stop offset="0" stopColor="var(--accent)" stopOpacity="0.0" />
            <stop offset="0.5" stopColor="var(--accent)" stopOpacity="0.4" />
            <stop offset="1" stopColor="var(--accent)" stopOpacity="0.0" />
          </linearGradient>
        </defs>
        <path
          d={shadowPath}
          fill="none"
          stroke="url(#vw-shadow)"
          strokeWidth="2.4"
          strokeLinecap="round"
          vectorEffect="non-scaling-stroke"
        />
        <path
          d={path}
          fill="none"
          stroke="url(#vw-grad)"
          strokeWidth="2.6"
          strokeLinecap="round"
          vectorEffect="non-scaling-stroke"
        />
      </svg>
    </div>
  );
}

window.VoiceWave = VoiceWave;

// ── Check-In Modal with voice → Voxtral transcription flow ─────────────
const SCOPE_ORDER = ["squad", "team", "group", "function", "business"];

function CheckInModal({
  open,
  onClose,
  onPost,
  currentScope,
  activeSquadId,
  userSquads,
  setActiveSquadId,
  checkinScope,
}) {
  const maxScopeIdx = SCOPE_ORDER.indexOf(checkinScope || "business");
  const selectedScopes = SCOPE_ORDER.slice(0, maxScopeIdx + 1);

  const [text, setText] = useState("");
  const [sentiment, setSentiment] = useState(null);
  const [selectedSquadIds, setSelectedSquadIds] = useState([]);
  const [recording, setRecording] = useState(false);
  const [recDuration, setRecDuration] = useState(0);
  const [recorded, setRecorded] = useState(null);
  const [recordedUrl, setRecordedUrl] = useState(null);
  const [transcript, setTranscript] = useState("");
  const [aiRows, setAiRows] = useState([]);
  const [aiThinking, setAiThinking] = useState(false);
  const [aiDemo, setAiDemo] = useState(false);
  const [transcribeError, setTranscribeError] = useState(null);
  const [posting, setPosting] = useState(false);
  const [postError, setPostError] = useState(null);
  const [availableGroups, setAvailableGroups] = useState([]);
  const [uploadedFiles, setUploadedFiles] = useState([]); // File objects
  const [fileProcessing, setFileProcessing] = useState(false);
  const [fileResult, setFileResult] = useState(null); // { transcript, signals, files }
  const [playing, setPlaying] = useState(false);
  const [textOpen, setTextOpen] = useState(false);
  const [meetingFile, setMeetingFile] = useState(null);
  const [meetingProcessing, setMeetingProcessing] = useState(false);
  const [meetingResult, setMeetingResult] = useState(null);
  const [meetingError, setMeetingError] = useState(null);
  const [showFullTranscript, setShowFullTranscript] = useState(false);
  const playRef = useRef(null);
  const fileInputRef = useRef(null);
  const meetingInputRef = useRef(null);

  const timerRef = useRef(null);
  const durRef = useRef(0);
  const wsRef = useRef(null);
  const audioCtxRef = useRef(null);
  const processorRef = useRef(null);
  const pcmBufferRef = useRef([]);
  const streamRef = useRef(null);
  const isLiveRef = useRef(false);
  const wsAudioRef = useRef(null); // {audio_key, audio_bucket, audio_duration}
  const analyserRef = useRef(null);
  const [liveAnalyser, setLiveAnalyser] = useState(null); // AnalyserNode while recording

  const DEMO_TRANSCRIPT =
    "Quick check-in Thursday. Two things moved: we decided to cut over to the new ingestion path on Friday at 2pm UTC, and the shadow-write on billing is showing a 0.2 percent drift. I want Marco to drive the drift call. We should write up the cutover runbook in the wiki. Blocker: I'm still waiting on the Kafka vendor throughput quote. Energy is steady — a bit stretched but doable.";

  const DEMO_SIGNALS = [
    {
      t: "DEC",
      kind: "dec",
      text: "Cut over to new ingestion path Friday 14:00 UTC",
      flow: "Added to Decisions in your squad",
    },
    {
      t: "BLK",
      kind: "blk",
      text: "Waiting on Kafka vendor throughput quote",
      flow: "Surfaced as Blocker; pinged Function lead",
    },
    {
      t: "KB",
      kind: "kb",
      text: "Suggest new article: Ingestion path cutover runbook",
      flow: "Added to Knowledge Base",
    },
    {
      t: "TASK",
      kind: "tsk",
      text: "Marco to drive shadow-write drift call",
      flow: "Added to Marco's to-do (delegated)",
    },
    {
      t: "PULSE",
      kind: "tsk",
      text: "Sentiment: steady · trending stretched",
      flow: "Logged in sentiment pulse",
    },
  ];

  const stopMicStream = () => {
    if (streamRef.current) {
      streamRef.current.getTracks().forEach((t) => t.stop());
      streamRef.current = null;
    }
  };

  const resetState = () => {
    clearInterval(timerRef.current);
    if (processorRef.current) {
      processorRef.current.disconnect();
      processorRef.current = null;
    }
    if (audioCtxRef.current) {
      audioCtxRef.current.close();
      audioCtxRef.current = null;
    }
    if (wsRef.current) {
      wsRef.current.close();
      wsRef.current = null;
    }
    stopMicStream();
    analyserRef.current = null;
    setLiveAnalyser(null);
    isLiveRef.current = false;
    durRef.current = 0;
    pcmBufferRef.current = [];
    setText("");
    setSentiment(null);
    setRecording(false);
    setRecDuration(0);
    setRecorded(null);
    setRecordedUrl(null);
    setTranscript("");
    setAiRows([]);
    setAiThinking(false);
    setPosting(false);
    setAiDemo(false);
    setTranscribeError(null);
    setUploadedFiles([]);
    setFileProcessing(false);
    setFileResult(null);
    setMeetingFile(null);
    setMeetingProcessing(false);
    setMeetingResult(null);
    setMeetingError(null);
    wsAudioRef.current = null;
  };

  const [showLeaveConfirm, setShowLeaveConfirm] = useState(false);

  useEffect(() => {
    if (!open) {
      resetState();
      setShowLeaveConfirm(false);
      setSelectedSquadIds([]);
    } else {
      setSelectedSquadIds(activeSquadId ? [activeSquadId] : []);
    }
  }, [open]);

  const hasContent = !!(
    text.trim() ||
    recorded ||
    transcript ||
    aiRows.length ||
    uploadedFiles.length ||
    fileResult ||
    meetingFile ||
    meetingResult
  );

  const handleClose = () => {
    if (hasContent) {
      setShowLeaveConfirm(true);
    } else {
      onClose();
    }
  };

  // Intercept Esc key to show confirmation if there's content
  useEffect(() => {
    if (!open) return;
    const handleEsc = (e) => {
      if (e.key === "Escape") {
        e.stopPropagation();
        e.preventDefault();
        handleClose();
      }
    };
    window.addEventListener("keydown", handleEsc, true);
    return () => window.removeEventListener("keydown", handleEsc, true);
  }, [open, text, recorded, transcript, aiRows, uploadedFiles, fileResult]);

  // Fetch user's groups when modal opens
  useEffect(() => {
    if (!open) return;
    const apiBase = window.PULSE_CONFIG?.apiBase || "http://localhost:8000";
    const token = localStorage.getItem("pulse_token");
    fetch(`${apiBase}/groups`, {
      headers: { Authorization: `Bearer ${token}` },
    })
      .then((r) => (r.ok ? r.json() : []))
      .then((g) => setAvailableGroups(g.filter((gr) => gr.is_member)))
      .catch(() => {});
  }, [open]);

  const getTranscribeUrl = () => {
    const url = window.PULSE_CONFIG?.transcribeUrl;
    return !url || url === "__TRANSCRIBE_URL__" ? null : url;
  };

  const getApiKey = () => {
    const key = window.PULSE_CONFIG?.apiKey;
    return !key || key === "__API_KEY__" ? null : key;
  };

  const streamWords = (words, onDone) => {
    let i = 0;
    const iv = setInterval(() => {
      i++;
      setTranscript(words.slice(0, i).join(" "));
      if (i >= words.length) {
        clearInterval(iv);
        onDone && onDone();
      }
    }, 45);
  };

  const streamDemoTranscript = () => {
    setAiDemo(true);
    streamWords(DEMO_TRANSCRIPT.split(" "), () => {
      DEMO_SIGNALS.forEach((sig, idx) =>
        setTimeout(() => setAiRows((r) => [...r, sig]), 280 * (idx + 1)),
      );
      setTimeout(() => setAiThinking(false), 280 * (DEMO_SIGNALS.length + 1));
    });
  };

  const TYPE_MAP = {
    decision: { t: "DEC", kind: "decision", css: "dec" },
    blocker: { t: "BLK", kind: "blocker", css: "blk" },
    task: { t: "TASK", kind: "task", css: "tsk" },
    delegation: { t: "TASK", kind: "task", css: "tsk" },
    knowledge: { t: "KB", kind: "knowledge", css: "kb" },
    experiment: { t: "EXP", kind: "experiment", css: "exp" },
    idea: { t: "IDEA", kind: "idea", css: "idea" },
    outcome: { t: "OUT", kind: "outcome", css: "out" },
  };

  const createWavBlob = (int16Samples, sampleRate) => {
    const dataLen = int16Samples.length * 2;
    const buf = new ArrayBuffer(44 + dataLen);
    const v = new DataView(buf);
    const str = (o, s) => {
      for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i));
    };
    str(0, "RIFF");
    v.setUint32(4, 36 + dataLen, true);
    str(8, "WAVE");
    str(12, "fmt ");
    v.setUint32(16, 16, true);
    v.setUint16(20, 1, true);
    v.setUint16(22, 1, true);
    v.setUint32(24, sampleRate, true);
    v.setUint32(28, sampleRate * 2, true);
    v.setUint16(32, 2, true);
    v.setUint16(34, 16, true);
    str(36, "data");
    v.setUint32(40, dataLen, true);
    new Int16Array(buf, 44).set(int16Samples);
    return new Blob([buf], { type: "audio/wav" });
  };

  const handleWsMessage = (msg) => {
    if (msg.type === "partial") {
      setTranscript(msg.transcript);
    } else if (msg.type === "final") {
      setTranscript(msg.transcript);
      setAiThinking(false);
      if (msg.audio_key)
        wsAudioRef.current = {
          audio_key: msg.audio_key,
          audio_bucket: msg.audio_bucket,
          audio_duration: msg.audio_duration,
        };
      const rows = (msg.signals || []).map((s) => {
        const meta = TYPE_MAP[s.type] || {
          t: s.type.slice(0, 4).toUpperCase(),
          kind: s.type,
          css: "tsk",
        };
        const sigText =
          s.type === "delegation" && s.assignee
            ? `${s.text} \u2192 ${s.assignee}`
            : s.text;
        return {
          ...meta,
          text: sigText,
          quote: s.quote || null,
          flow: s.flow || "",
        };
      });
      rows.forEach((sig, idx) =>
        setTimeout(() => setAiRows((r) => [...r, sig]), 280 * (idx + 1)),
      );
    } else if (msg.type === "error") {
      setTranscribeError(msg.message);
      setAiThinking(false);
      setRecorded(null);
      setRecordedUrl(null);
      wsAudioRef.current = null;
    }
  };

  const stopRecording = () => {
    clearInterval(timerRef.current);

    if (processorRef.current) {
      processorRef.current.disconnect();
      processorRef.current = null;
    }
    if (audioCtxRef.current) {
      audioCtxRef.current.close();
      audioCtxRef.current = null;
    }
    analyserRef.current = null;
    setLiveAnalyser(null);

    // Build WAV blob from buffered PCM for immediate playback
    const pcm = pcmBufferRef.current;
    if (pcm && pcm.length > 0) {
      const total = pcm.reduce((n, b) => n + b.length, 0);
      const combined = new Int16Array(total);
      let off = 0;
      for (const chunk of pcm) {
        combined.set(chunk, off);
        off += chunk.length;
      }
      setRecordedUrl(URL.createObjectURL(createWavBlob(combined, 24000)));
    }
    pcmBufferRef.current = [];

    stopMicStream();
    isLiveRef.current = false;

    const dur = Math.max(1, durRef.current);
    setRecorded({ duration: dur });
    setRecording(false);

    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      wsRef.current.send("END");
      setAiThinking(true);
    } else if (!wsRef.current) {
      setAiThinking(true);
      streamDemoTranscript();
    }
  };

  const startRecording = async () => {
    setRecording(true);
    setRecDuration(0);
    durRef.current = 0;
    setRecorded(null);
    setTranscript("");
    setAiRows([]);
    setAiDemo(false);
    setTranscribeError(null);

    timerRef.current = setInterval(() => {
      durRef.current += 0.1;
      setRecDuration((d) => d + 0.1);
    }, 100);

    const url = getTranscribeUrl();
    const apiKey = getApiKey();

    if (url && navigator.mediaDevices?.getUserMedia) {
      try {
        // Tighter constraints reduce background / distant voices. autoGainControl
        // off so the mic doesn't boost faint far-away speech up to a usable level.
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: {
            noiseSuppression: true,
            echoCancellation: true,
            autoGainControl: false,
          },
        });
        streamRef.current = stream;

        // Build WebSocket URL: http → ws, /transcribe → /transcribe-stream
        const token = localStorage.getItem("pulse_token") || "";
        const wsUrl =
          url
            .replace(/^http/, "ws")
            .replace(/\/transcribe$/, "/transcribe-stream") +
          "?api_key=" +
          encodeURIComponent(apiKey || "") +
          "&token=" +
          encodeURIComponent(token) +
          (activeSquadId
            ? "&squad_id=" + encodeURIComponent(activeSquadId)
            : "");
        const ws = new WebSocket(wsUrl);
        wsRef.current = ws;
        ws.binaryType = "arraybuffer";
        ws.onmessage = (e) => handleWsMessage(JSON.parse(e.data));
        ws.onerror = () => {
          setTranscribeError("Connection to transcription service failed");
          setAiThinking(false);
        };

        // AudioContext at 24 kHz — GPT Realtime API requires 24000 Hz PCM16 mono
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)(
          { sampleRate: 24000 },
        );
        audioCtxRef.current = audioCtx;
        const source = audioCtx.createMediaStreamSource(stream);
        const processor = audioCtx.createScriptProcessor(4096, 1, 1);
        processorRef.current = processor;
        pcmBufferRef.current = [];

        // Live frequency analyser for the reactive waveform visual
        const analyser = audioCtx.createAnalyser();
        analyser.fftSize = 256;
        analyser.smoothingTimeConstant = 0.8;
        source.connect(analyser);
        analyserRef.current = analyser;
        setLiveAnalyser(analyser);

        ws.onopen = () => {
          setAiThinking(true);
          // Flush any PCM captured while WebSocket was connecting
          for (const chunk of pcmBufferRef.current) ws.send(chunk.buffer);
        };

        processor.onaudioprocess = (e) => {
          const float32 = e.inputBuffer.getChannelData(0);
          const int16 = new Int16Array(float32.length);
          for (let i = 0; i < float32.length; i++) {
            const s = Math.max(-1, Math.min(1, float32[i]));
            int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
          }
          pcmBufferRef.current.push(int16);
          if (ws.readyState === WebSocket.OPEN) ws.send(int16.buffer);
        };

        source.connect(processor);
        processor.connect(audioCtx.destination);
        isLiveRef.current = true;
        return;
      } catch (err) {
        console.warn("Streaming setup failed, using demo mode:", err);
      }
    }
    // Demo mode — auto-stop via recDuration useEffect below
  };

  // Auto-stop: 120s for live recording, 6s for demo
  useEffect(() => {
    if (!recording) return;
    const cap = isLiveRef.current ? 120 : 6;
    if (recDuration >= cap) stopRecording();
  }, [recDuration]);

  if (!open) return null;

  const canPost =
    !!(text.trim() || recorded || uploadedFiles.length > 0 || meetingResult) &&
    !posting &&
    !fileProcessing;

  // Analyse typed text → show the same animated signal preview as voice.
  const analyzeText = async () => {
    const note = text.trim();
    if (!note || aiThinking) return;
    const apiBase = window.PULSE_CONFIG?.apiBase || "http://localhost:8000";
    const token = localStorage.getItem("pulse_token");
    setAiRows([]);
    setAiThinking(true);
    try {
      const res = await fetch(`${apiBase}/check-ins/preview-signals`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({ transcript: note }),
      });
      const data = res.ok ? await res.json() : { signals: [] };
      const rows = (data.signals || []).map((s) => {
        const kind = s.kind || s.type;
        const meta = TYPE_MAP[kind] || {
          t: (kind || "?").slice(0, 4).toUpperCase(),
          kind,
          css: "tsk",
        };
        return {
          ...meta,
          text: s.body || s.text || "",
          quote: s.quote || null,
          flow: s.flow || "",
        };
      });
      // Stagger reveal like the voice flow
      setAiThinking(false);
      rows.forEach((sig, idx) =>
        setTimeout(() => setAiRows((r) => [...r, sig]), 220 * (idx + 1)),
      );
      if (!rows.length) setAiRows([]);
    } catch {
      setAiThinking(false);
    }
  };

  const handleMeetingUpload = async (file) => {
    const apiBase = window.PULSE_CONFIG?.apiBase || "http://localhost:8000";
    const token = localStorage.getItem("pulse_token");
    setMeetingFile(file);
    setMeetingProcessing(true);
    setMeetingError(null);
    setMeetingResult(null);
    try {
      const formData = new FormData();
      formData.append("file", file);
      formData.append("scopes", JSON.stringify(selectedScopes));
      if (sentiment) formData.append("sentiment", sentiment);
      if (activeSquadId) formData.append("squad_id", activeSquadId);
      const res = await fetch(`${apiBase}/check-ins/meeting-transcript`, {
        method: "POST",
        headers: { Authorization: `Bearer ${token}` },
        body: formData,
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        setMeetingError(err.detail || "Failed to process transcript");
        setMeetingProcessing(false);
        return;
      }
      const data = await res.json();
      setMeetingResult(data);
      setTranscript(data.transcript);
      const rows = (data.signals || []).map((s) => {
        const meta = TYPE_MAP[s.kind] || {
          t: (s.kind || "?").slice(0, 4).toUpperCase(),
          kind: s.kind,
          css: "tsk",
        };
        return {
          ...meta,
          text: s.body || "",
          quote: s.quote || null,
          flow: "",
          assignee: s.assignee || null,
        };
      });
      rows.forEach((sig, idx) =>
        setTimeout(() => setAiRows((r) => [...r, sig]), 220 * (idx + 1)),
      );
      setMeetingProcessing(false);
    } catch (e) {
      console.error("Meeting transcript error:", e);
      setMeetingError("Something went wrong processing the transcript");
      setMeetingProcessing(false);
    }
  };

  const handlePost = async () => {
    const apiBase = window.PULSE_CONFIG?.apiBase || "http://localhost:8000";
    const token = localStorage.getItem("pulse_token");

    // Meeting transcript check-in (already saved by handleMeetingUpload)
    if (meetingResult) {
      onPost({
        id: meetingResult.id,
        text: meetingResult.transcript,
        fullTranscript: meetingResult.full_transcript || null,
        sentiment: sentiment || "Focused",
        voice: null,
        meeting: true,
        aiRows: aiRows,
        scopes: selectedScopes,
        completedTodoSuggestions:
          meetingResult.completed_todo_suggestions || [],
        otherCheckIns: meetingResult.other_check_ins || [],
      });
      return;
    }

    // File upload check-in (files present — combine with any text/voice transcript)
    if (uploadedFiles.length > 0) {
      setPosting(true);
      setFileProcessing(true);
      try {
        const formData = new FormData();
        uploadedFiles.forEach((f) => formData.append("files", f));
        // Combine typed text and voice transcript into a single comment for context
        const parts = [];
        if (text.trim()) parts.push(text.trim());
        if (transcript) parts.push(transcript);
        const combined = parts.join("\n\n");
        if (combined) formData.append("comment", combined);
        formData.append("scopes", JSON.stringify(selectedScopes));
        if (sentiment) formData.append("sentiment", sentiment);

        const res = await fetch(`${apiBase}/check-ins/file`, {
          method: "POST",
          headers: { Authorization: `Bearer ${token}` },
          body: formData,
        });
        if (!res.ok) {
          const err = await res.json().catch(() => ({}));
          setPostError(err.detail || "Failed to post check-in");
          setPosting(false);
          setFileProcessing(false);
          return;
        }
        const data = await res.json();
        setFileResult(data);
        const serverAiRows = (data.signals || []).map((s) => ({
          kind: s.kind,
          text: s.body,
        }));
        setTimeout(() => {
          onPost({
            text: data.transcript,
            sentiment: sentiment || "Focused",
            voice: null,
            aiRows: serverAiRows,
            scopes: selectedScopes,
          });
          setPosting(false);
          setFileProcessing(false);
        }, 300);
      } catch (e) {
        console.error("File check-in error:", e);
        setPosting(false);
        setFileProcessing(false);
      }
      return;
    }

    // All check-ins (text and voice) are posted via the same API call
    const finalTranscript = text.trim() || transcript;
    if (!finalTranscript) return;

    setPosting(true);
    try {
      const body = {
        transcript: finalTranscript,
        sentiment,
        scopes: selectedScopes,
        squad_ids:
          selectedSquadIds.length > 0
            ? selectedSquadIds
            : activeSquadId
              ? [activeSquadId]
              : undefined,
      };
      // For voice check-ins, include audio metadata and pre-extracted signals
      if (recorded && wsAudioRef.current) {
        body.audio_key = wsAudioRef.current.audio_key;
        body.audio_bucket = wsAudioRef.current.audio_bucket;
        body.audio_duration = wsAudioRef.current.audio_duration;
      }
      // Reuse signals already shown in the preview (voice or analysed text) so
      // the saved check-in matches the tags on screen and the API skips re-extraction.
      if (aiRows.length) {
        body.signals = aiRows.map((r) => ({ kind: r.kind, body: r.text }));
      }
      const res = await fetch(`${apiBase}/check-ins`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify(body),
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        setPostError(err.detail || "Failed to post check-in");
        setPosting(false);
        return;
      }
      const data = await res.json();
      const serverAiRows = (data.signals || []).map((s) => ({
        kind: s.kind,
        text: s.body,
        assignee: s.assignee || null,
      }));
      onPost({
        id: data.id,
        text: finalTranscript,
        sentiment,
        voice: recorded
          ? {
              duration: Math.round(recorded.duration),
              transcript,
              url: recordedUrl,
            }
          : null,
        aiRows: serverAiRows.length ? serverAiRows : aiRows,
        scopes: selectedScopes,
        completedTodoSuggestions: data.completed_todo_suggestions || [],
      });
    } catch (e) {
      console.error("Check-in POST error:", e);
    } finally {
      setPosting(false);
    }
  };

  if (checkinScope === "none") {
    return (
      <div
        className="modal-backdrop"
        onMouseDown={(e) => {
          if (e.target === e.currentTarget) onClose();
        }}
      >
        <div className="modal" role="dialog" aria-label="Check in">
          <div className="modal-head">
            <div className="t">
              <span className="dot" />
              Check In
            </div>
            <button
              className="modal-close"
              onClick={onClose}
              aria-label="Close"
            >
              <Icon.Close />
            </button>
          </div>
          <div
            className="modal-body"
            style={{ padding: "40px 24px", textAlign: "center" }}
          >
            <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
              Check-ins are not available
            </div>
            <div
              style={{ fontSize: 13, color: "var(--ink-3)", lineHeight: 1.5 }}
            >
              Your access group does not have permission to post check-ins.
              Please contact your administrator if you believe this is an error.
            </div>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div
      className="modal-backdrop"
      onMouseDown={(e) => {
        if (e.target === e.currentTarget) handleClose();
      }}
    >
      <div className="modal" role="dialog" aria-label="Check in">
        <div className="modal-head">
          <div className="t">
            <span className="dot" />
            Check In <span className="when">Thu 12:20</span>
          </div>
          <button
            className="modal-close"
            onClick={handleClose}
            aria-label="Close"
          >
            <Icon.Close />
          </button>
        </div>

        <div className="modal-body">
          {/* ── Voice hero ─────────────────────────────── */}
          <div
            className={`voice-hero ${recording ? "is-recording" : ""} ${recorded ? "is-recorded" : ""}`}
          >
            <div className="voice-hero-prompt">
              {!recording && !recorded && (
                <>
                  <div className="voice-hero-title">What moved today?</div>
                  <div className="voice-hero-sub">
                    Speak freely · Pulse turns it into decisions, blockers, and
                    follow-ups.
                  </div>
                </>
              )}
              {recording && (
                <>
                  <div className="voice-hero-title recording-title">
                    <span className="rec-dot" />
                    Listening
                  </div>
                  <div className="voice-hero-sub">
                    {recDuration.toFixed(1)}s · up to 2 min · tap to stop
                  </div>
                </>
              )}
              {recorded && !recording && (
                <>
                  <div className="voice-hero-title">
                    Recorded · {Math.round(recorded.duration)}s
                  </div>
                  <div className="voice-hero-sub">
                    Replay, re-record, or post when you're ready.
                  </div>
                </>
              )}
            </div>

            <div className="voice-hero-stage">
              <VoiceWave
                recording={recording}
                recorded={!!recorded}
                analyser={recording ? liveAnalyser : null}
              />
            </div>

            <div className="voice-hero-controls">
              {!recorded ? (
                <button
                  className={`mic-fab ${recording ? "recording" : ""}`}
                  onClick={() =>
                    recording ? stopRecording() : startRecording()
                  }
                  aria-label={recording ? "Stop recording" : "Start recording"}
                >
                  {recording ? (
                    <span className="stop-icon" />
                  ) : (
                    <Icon.Mic style={{ width: 22, height: 22 }} />
                  )}
                </button>
              ) : (
                <div className="voice-hero-after">
                  {recordedUrl && (
                    <button
                      className="mic-fab small"
                      onClick={() => {
                        if (playing && playRef.current) {
                          playRef.current.pause();
                          playRef.current = null;
                          setPlaying(false);
                        } else {
                          const a = new Audio(recordedUrl);
                          playRef.current = a;
                          setPlaying(true);
                          a.onended = () => {
                            setPlaying(false);
                            playRef.current = null;
                          };
                          a.play();
                        }
                      }}
                    >
                      {playing ? (
                        <svg
                          viewBox="0 0 24 24"
                          fill="currentColor"
                          style={{ width: 18, height: 18 }}
                          aria-hidden="true"
                        >
                          <path d="M6 4h4v16H6zm8 0h4v16h-4z" />
                        </svg>
                      ) : (
                        <svg
                          viewBox="0 0 24 24"
                          fill="currentColor"
                          style={{ width: 18, height: 18 }}
                          aria-hidden="true"
                        >
                          <path d="M8 5v14l11-7z" />
                        </svg>
                      )}
                    </button>
                  )}
                  <button
                    className="ghost-btn"
                    onClick={() => {
                      if (playRef.current) {
                        playRef.current.pause();
                        playRef.current = null;
                        setPlaying(false);
                      }
                      setRecorded(null);
                      setRecordedUrl(null);
                      setTranscript("");
                      setAiRows([]);
                      setAiDemo(false);
                    }}
                  >
                    Re-record
                  </button>
                </div>
              )}
            </div>
          </div>

          {/* ── Or, write a note ───────────────────────── */}
          {!textOpen && !text ? (
            <button className="text-toggle" onClick={() => setTextOpen(true)}>
              <Icon.Edit style={{ width: 13, height: 13 }} />
              Or jot a quick note
            </button>
          ) : (
            <div className="field text-field">
              <textarea
                className="checkin-text-area"
                placeholder="A sentence is enough. Nothing to report is fine too."
                value={text}
                onChange={(e) => {
                  setText(e.target.value);
                  if (aiRows.length || transcribeError) {
                    setAiRows([]);
                    setTranscribeError(null);
                  }
                }}
                autoFocus={textOpen}
              />
              {text.trim() && !recorded && (
                <div
                  style={{
                    display: "flex",
                    justifyContent: "flex-end",
                    marginTop: 6,
                  }}
                >
                  <button
                    type="button"
                    className="btn"
                    style={{ fontSize: 12 }}
                    disabled={aiThinking}
                    onClick={analyzeText}
                  >
                    <Icon.Spark
                      style={{
                        width: 12,
                        height: 12,
                        marginRight: 4,
                        verticalAlign: -1,
                      }}
                    />
                    {aiThinking
                      ? "Analysing…"
                      : aiRows.length
                        ? "Re-analyse"
                        : "Analyse with AI"}
                  </button>
                </div>
              )}
            </div>
          )}

          {!transcript && !aiRows.length && !recorded && (
            <>
              <div className="field">
                <label>
                  Upload files{" "}
                  <span
                    style={{
                      color: "var(--ink-3)",
                      fontWeight: 400,
                      fontSize: 12,
                      marginLeft: 4,
                    }}
                  >
                    PDF, Word, PowerPoint, Excel, images, text
                  </span>
                </label>
                <input
                  ref={fileInputRef}
                  type="file"
                  multiple
                  accept=".pdf,.docx,.pptx,.xlsx,.xls,.txt,.md,.csv,.log,.json,.yaml,.yml,.png,.jpg,.jpeg,.gif,.webp"
                  style={{ display: "none" }}
                  onChange={(e) => {
                    const newFiles = Array.from(e.target.files || []);
                    setUploadedFiles((prev) =>
                      [...prev, ...newFiles].slice(0, 5),
                    );
                    e.target.value = "";
                  }}
                />
                {uploadedFiles.length === 0 ? (
                  <div
                    className="file-drop-zone"
                    onClick={() => fileInputRef.current?.click()}
                    onDragOver={(e) => {
                      e.preventDefault();
                      e.currentTarget.classList.add("dragover");
                    }}
                    onDragLeave={(e) => {
                      e.currentTarget.classList.remove("dragover");
                    }}
                    onDrop={(e) => {
                      e.preventDefault();
                      e.currentTarget.classList.remove("dragover");
                      const newFiles = Array.from(e.dataTransfer.files || []);
                      setUploadedFiles((prev) =>
                        [...prev, ...newFiles].slice(0, 5),
                      );
                    }}
                  >
                    <Icon.File
                      style={{ width: 20, height: 20, color: "var(--ink-3)" }}
                    />
                    <span>Drop files here or click to browse</span>
                    <span className="file-hint">
                      AI will read and summarise the content for your check-in
                    </span>
                  </div>
                ) : (
                  <div className="file-list">
                    {uploadedFiles.map((f, i) => (
                      <div key={i} className="file-item">
                        <Icon.File
                          style={{ width: 14, height: 14, flexShrink: 0 }}
                        />
                        <span className="file-name">{f.name}</span>
                        <span className="file-size">
                          {(f.size / 1024).toFixed(0)}KB
                        </span>
                        <button
                          className="file-remove"
                          onClick={() =>
                            setUploadedFiles((prev) =>
                              prev.filter((_, j) => j !== i),
                            )
                          }
                          aria-label="Remove file"
                        >
                          &times;
                        </button>
                      </div>
                    ))}
                    {uploadedFiles.length < 5 && (
                      <button
                        className="btn"
                        style={{
                          fontSize: 12,
                          padding: "4px 10px",
                          marginTop: 6,
                        }}
                        onClick={() => fileInputRef.current?.click()}
                      >
                        + Add more files
                      </button>
                    )}
                  </div>
                )}
                {fileProcessing && (
                  <div
                    style={{
                      fontSize: 12,
                      color: "var(--accent)",
                      marginTop: 8,
                      display: "flex",
                      alignItems: "center",
                      gap: 6,
                    }}
                  >
                    <Icon.Spark style={{ width: 12, height: 12 }} /> AI is
                    reading your files…
                  </div>
                )}
              </div>

              {/* ── Teams meeting transcript import ─────── */}
              <div className="field">
                <label>
                  Import Teams meeting{" "}
                  <span
                    style={{
                      color: "var(--ink-3)",
                      fontWeight: 400,
                      fontSize: 12,
                      marginLeft: 4,
                    }}
                  >
                    .vtt transcript
                  </span>
                </label>
                <input
                  ref={meetingInputRef}
                  type="file"
                  accept=".vtt"
                  style={{ display: "none" }}
                  onChange={(e) => {
                    const f = e.target.files?.[0];
                    if (f) handleMeetingUpload(f);
                    e.target.value = "";
                  }}
                />
                {!meetingFile && !meetingResult ? (
                  <div
                    className="file-drop-zone meeting-drop"
                    onClick={() => meetingInputRef.current?.click()}
                    onDragOver={(e) => {
                      e.preventDefault();
                      e.currentTarget.classList.add("dragover");
                    }}
                    onDragLeave={(e) => {
                      e.currentTarget.classList.remove("dragover");
                    }}
                    onDrop={(e) => {
                      e.preventDefault();
                      e.currentTarget.classList.remove("dragover");
                      const f = e.dataTransfer.files?.[0];
                      if (f && f.name.toLowerCase().endsWith(".vtt")) {
                        handleMeetingUpload(f);
                      } else {
                        setMeetingError("Please drop a .vtt file");
                      }
                    }}
                  >
                    <svg
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.5"
                      style={{ width: 20, height: 20, color: "var(--ink-3)" }}
                    >
                      <path d="M15 10l-4 4l6 6l4-16l-18 7l4 2l2 6l3-4" />
                    </svg>
                    <span>Drop Teams transcript or click to browse</span>
                    <span className="file-hint">
                      AI extracts your contributions and identifies actions,
                      decisions &amp; blockers. Check-ins are also created for
                      recognised squad members in the transcript.
                    </span>
                  </div>
                ) : (
                  <div className="meeting-result-card">
                    <div className="meeting-file-row">
                      <svg
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth="1.5"
                        style={{
                          width: 16,
                          height: 16,
                          flexShrink: 0,
                          color: "var(--accent)",
                        }}
                      >
                        <path d="M15 10l-4 4l6 6l4-16l-18 7l4 2l2 6l3-4" />
                      </svg>
                      <span style={{ flex: 1, fontSize: 13 }}>
                        {meetingFile?.name}
                      </span>
                      {meetingResult && (
                        <span
                          style={{
                            fontSize: 11,
                            color: "var(--ink-3)",
                            background: "var(--bg-2)",
                            padding: "2px 8px",
                            borderRadius: 10,
                          }}
                        >
                          Matched as: {meetingResult.matched_speaker}
                        </span>
                      )}
                      {!meetingProcessing && (
                        <button
                          className="file-remove"
                          onClick={() => {
                            setMeetingFile(null);
                            setMeetingResult(null);
                            setMeetingError(null);
                            setTranscript("");
                            setAiRows([]);
                          }}
                          aria-label="Remove"
                        >
                          &times;
                        </button>
                      )}
                    </div>
                    {meetingProcessing && (
                      <div
                        style={{
                          fontSize: 12,
                          color: "var(--accent)",
                          marginTop: 8,
                          display: "flex",
                          alignItems: "center",
                          gap: 6,
                        }}
                      >
                        <Icon.Spark style={{ width: 12, height: 12 }} />
                        Extracting your contributions and analysing…
                      </div>
                    )}
                    {meetingResult && meetingResult.speakers && (
                      <div
                        style={{
                          fontSize: 11,
                          color: "var(--ink-3)",
                          marginTop: 6,
                        }}
                      >
                        {meetingResult.speakers.length} participants in meeting
                      </div>
                    )}
                  </div>
                )}
                {meetingError && (
                  <div
                    style={{
                      fontSize: 12,
                      color: "var(--danger)",
                      background: "var(--danger-soft)",
                      border: "1px solid #f5c0c0",
                      borderRadius: 8,
                      padding: "8px 12px",
                      marginTop: 8,
                    }}
                  >
                    {meetingError}
                  </div>
                )}
              </div>
            </>
          )}

          {(transcript ||
            aiRows.length > 0 ||
            aiThinking ||
            transcribeError) && (
            <div className="field">
              <label style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <Icon.Spark
                  style={{ width: 14, height: 14, color: "var(--accent)" }}
                />
                AI transcript &amp; signals
              </label>
              {transcribeError && (
                <div
                  style={{
                    fontSize: 12,
                    color: "var(--danger)",
                    background: "var(--danger-soft)",
                    border: "1px solid #f5c0c0",
                    borderRadius: 8,
                    padding: "8px 12px",
                    marginBottom: 8,
                  }}
                >
                  ⚠ Transcription failed: {transcribeError}
                </div>
              )}
              {transcript && (
                <div
                  className="voice-transcript editable-transcript"
                  style={{ marginBottom: 10, position: "relative" }}
                >
                  <span
                    className="transcript-text"
                    contentEditable
                    suppressContentEditableWarning
                    onBlur={(e) => setTranscript(e.currentTarget.textContent)}
                  >
                    {transcript}
                  </span>
                  {aiThinking && !aiRows.length && (
                    <span style={{ opacity: 0.4 }}>|</span>
                  )}
                  <span
                    className="transcript-edit-icon"
                    aria-label="Edit transcript"
                  >
                    <Icon.Edit style={{ width: 12, height: 12 }} />
                  </span>
                </div>
              )}
              {meetingResult && meetingResult.full_transcript && (
                <>
                  <button
                    onClick={() => setShowFullTranscript((v) => !v)}
                    style={{
                      display: "block",
                      marginBottom: 10,
                      fontSize: 12,
                      color: "var(--accent)",
                      background: "none",
                      border: "none",
                      cursor: "pointer",
                      padding: 0,
                      fontWeight: 500,
                    }}
                  >
                    {showFullTranscript
                      ? "Hide full transcript"
                      : "Show full transcript"}
                  </button>
                  {showFullTranscript && (
                    <div
                      className="voice-transcript"
                      style={{
                        marginBottom: 10,
                        maxHeight: 300,
                        overflowY: "auto",
                        fontSize: 12.5,
                        color: "var(--ink-2)",
                        whiteSpace: "pre-wrap",
                      }}
                    >
                      {meetingResult.full_transcript}
                    </div>
                  )}
                </>
              )}
              {(aiThinking || aiRows.length > 0) &&
                (aiThinking ? (
                  <div className="ai-preview">
                    <div className="ph">
                      <Icon.Spark />
                      Listening for decisions, blockers, tasks…
                    </div>
                    {aiRows.length === 0 && (
                      <div className="empty">Analysing…</div>
                    )}
                    {aiRows.map((r, i) => (
                      <div className="row fan-new" key={i}>
                        <span className={`t ${r.css || r.kind}`}>{r.t}</span>
                        <div style={{ flex: 1 }}>
                          <div>{r.text}</div>
                          <div
                            style={{
                              fontSize: 11.5,
                              color: "var(--ink-3)",
                              marginTop: 2,
                            }}
                          >
                            → {r.flow}
                          </div>
                        </div>
                      </div>
                    ))}
                  </div>
                ) : (
                  <SignalFanOut
                    aiRows={aiRows.map((r) => ({
                      kind: r.kind,
                      text: r.text,
                      importance: r.importance || 3,
                    }))}
                  />
                ))}
            </div>
          )}

          {/* Squad picker — multi-select for multi-squad users */}
          {userSquads && userSquads.length > 1 && (
            <div className="field">
              <label>Post to squads</label>
              <div className="scope-selector">
                {userSquads.map((s) => (
                  <button
                    key={s.id}
                    className={`scope-pill${selectedSquadIds.includes(s.id) ? " active" : ""}`}
                    onClick={() =>
                      setSelectedSquadIds((prev) =>
                        prev.includes(s.id)
                          ? prev.length > 1
                            ? prev.filter((id) => id !== s.id)
                            : prev
                          : [...prev, s.id],
                      )
                    }
                  >
                    {s.name}
                  </button>
                ))}
              </div>
            </div>
          )}

          <div className="field">
            <label>How does the work feel?</label>
            <div className="sentiment-row">
              {[
                "Steady",
                "Focused",
                "Stretched",
                "Stuck",
                "Unmotivated",
                "Energised",
              ].map((s) => (
                <button
                  key={s}
                  aria-pressed={sentiment === s}
                  onClick={() => setSentiment(s)}
                >
                  {s}
                </button>
              ))}
            </div>
            <div className="sentiment-hint">
              Never shown as a score. Only trends over time.
            </div>
          </div>
        </div>

        <div className="modal-foot">
          <div className="l">
            {postError ? (
              <span style={{ fontSize: 12, color: "var(--danger)" }}>
                {postError}
              </span>
            ) : (
              <span className="kbd">Esc</span>
            )}
            {postError ? null : " to close"}
          </div>
          <div className="r">
            <button
              className="btn primary"
              disabled={!canPost}
              onClick={() => {
                setPostError(null);
                handlePost();
              }}
            >
              {posting && <span className="sp" />}
              {posting ? "Posting…" : "Post check-in"}
            </button>
          </div>
        </div>

        {showLeaveConfirm && (
          <div
            style={{
              position: "absolute",
              inset: 0,
              background: "rgba(0,0,0,0.4)",
              borderRadius: "inherit",
              display: "grid",
              placeItems: "center",
              zIndex: 10,
            }}
            onKeyDown={(e) => {
              if (e.key === "Enter") {
                e.preventDefault();
                onClose();
              }
            }}
          >
            <div
              style={{
                background: "var(--bg)",
                border: "1px solid var(--line)",
                borderRadius: 12,
                padding: "20px 24px",
                maxWidth: 320,
                textAlign: "center",
                boxShadow: "0 8px 24px rgba(0,0,0,0.2)",
              }}
              tabIndex={-1}
              ref={(el) => el && el.focus()}
            >
              <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 6 }}>
                Discard check-in?
              </div>
              <div
                style={{
                  fontSize: 13,
                  color: "var(--ink-3)",
                  marginBottom: 16,
                }}
              >
                Your current check-in will be lost.
              </div>
              <div
                style={{ display: "flex", gap: 8, justifyContent: "center" }}
              >
                <button
                  className="btn"
                  onClick={() => setShowLeaveConfirm(false)}
                >
                  Keep editing
                </button>
                <button
                  className="btn"
                  style={{ color: "var(--danger)" }}
                  onClick={onClose}
                >
                  Discard
                </button>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

window.CheckInModal = CheckInModal;
