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

// ── Quarter AI Summary ───────────────────────────────────────
function AISummaryCard({
  authToken,
  scope,
  activeSquadId,
  roleFilter,
  period,
}) {
  const apiBase = window.PULSE_CONFIG?.apiBase || "http://localhost:8000";
  const headers = { Authorization: `Bearer ${authToken}` };
  const scopeParam = scope ? `&scope=${encodeURIComponent(scope)}` : "";
  const squadParam = activeSquadId
    ? `&squad_id=${encodeURIComponent(activeSquadId)}`
    : "";
  const roleParam =
    roleFilter && roleFilter !== "all"
      ? `&role_tag=${encodeURIComponent(roleFilter)}`
      : "";
  // The summary is scoped to the globally-selected period (header selector).
  const periodKey = period?.key || "";
  const periodParam = period
    ? `&period=${encodeURIComponent(period.key)}&start=${encodeURIComponent(period.start)}&end=${encodeURIComponent(period.end)}`
    : "";

  const [report, setReport] = useState(null); // { bullets, source_count, generated_at } | null
  const [generating, setGenerating] = useState(false);
  const [error, setError] = useState(null);
  const [showDetailPicker, setShowDetailPicker] = useState(false);

  // Load any stored report whenever the period / scope / role changes
  useEffect(() => {
    if (!authToken || !period) return;
    setReport(null);
    setError(null);
    fetch(
      `${apiBase}/reports?period=${encodeURIComponent(periodKey)}${scopeParam}${squadParam}${roleParam}`,
      { headers },
    )
      .then((r) => (r.ok ? r.json() : null))
      .then((data) => {
        if (data && data.bullets) setReport(data);
      })
      .catch(() => {});
  }, [periodKey, scope, activeSquadId, roleFilter]);

  const handleGenerate = useCallback(
    async (detail) => {
      if (!period || generating) return;
      setShowDetailPicker(false);
      setGenerating(true);
      setError(null);
      try {
        const r = await fetch(
          `${apiBase}/reports/generate?${periodParam.slice(1)}${scopeParam}${squadParam}${roleParam}&detail=${encodeURIComponent(detail)}`,
          { method: "POST", headers },
        );
        if (!r.ok) {
          const err = await r.json().catch(() => ({ detail: "Failed" }));
          throw new Error(err.detail || `HTTP ${r.status}`);
        }
        const data = await r.json();
        setReport(data);
      } catch (e) {
        setError(e.message);
      } finally {
        setGenerating(false);
      }
    },
    [
      periodKey,
      periodParam,
      generating,
      authToken,
      roleFilter,
      scope,
      activeSquadId,
    ],
  );

  return (
    <div className="ai-card">
      <div className="ai-head">
        <span className="ai-label">
          <Icon.Spark />
          AI summary
          <span
            style={{ fontWeight: 500, color: "var(--ink-3)", marginLeft: 6 }}
          >
            · {period?.label}
          </span>
          {roleFilter && roleFilter !== "all" && (
            <span
              style={{ fontWeight: 500, color: "var(--accent)", marginLeft: 6 }}
            >
              · {roleFilter}
            </span>
          )}
          {report && (
            <span
              className="ai-source"
              style={{ fontWeight: 400, color: "var(--ink-3)", marginLeft: 6 }}
            >
              · sourced from {report.source_count} check-in
              {report.source_count !== 1 ? "s" : ""}
            </span>
          )}
        </span>
        <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
          <div style={{ position: "relative" }}>
            <button
              className="btn accent"
              style={{
                fontSize: 12,
                padding: "5px 12px",
                borderRadius: 6,
                display: "inline-flex",
                alignItems: "center",
                gap: 5,
              }}
              onClick={() => setShowDetailPicker((p) => !p)}
              disabled={generating || !period}
            >
              {generating ? (
                <>
                  <span className="sp" />
                  Generating…
                </>
              ) : (
                <>
                  <Icon.Spark style={{ width: 13, height: 13 }} />
                  {report ? "Regenerate" : "Generate report"}
                </>
              )}
            </button>
            {showDetailPicker && (
              <div className="detail-picker">
                <button
                  className="detail-option"
                  onClick={() => handleGenerate("executive")}
                >
                  <strong>Executive</strong>
                  <span>3 bullet points</span>
                </button>
                <button
                  className="detail-option"
                  onClick={() => handleGenerate("brief")}
                >
                  <strong>Brief</strong>
                  <span>5 sentences</span>
                </button>
                <button
                  className="detail-option"
                  onClick={() => handleGenerate("detailed")}
                >
                  <strong>Detailed</strong>
                  <span>Full paragraph</span>
                </button>
              </div>
            )}
          </div>
        </div>
      </div>

      {/* Body */}
      {generating && (
        <div
          className="ai-body"
          style={{ color: "var(--ink-3)", fontStyle: "italic", fontSize: 13 }}
        >
          Analysing {period?.label} check-ins…
        </div>
      )}
      {!generating && error && (
        <div
          className="ai-body"
          style={{ color: "var(--red, #c0392b)", fontSize: 13 }}
        >
          {error}
        </div>
      )}
      {!generating && !error && report && (
        <div className="ai-body">
          {Array.isArray(report.bullets) && report.bullets.length > 1 ? (
            <ul>
              {report.bullets.map((b, i) => (
                <li key={i}>{b}</li>
              ))}
            </ul>
          ) : (
            <p style={{ margin: 0, lineHeight: 1.6 }}>
              {Array.isArray(report.bullets)
                ? report.bullets[0]
                : report.bullets}
            </p>
          )}
        </div>
      )}
      {!generating && !error && !report && (
        <div
          className="ai-body"
          style={{ color: "var(--ink-3)", fontStyle: "italic", fontSize: 13 }}
        >
          No report yet for {period?.label || "this period"}. Click "Generate
          report" to summarise your check-ins with AI.
        </div>
      )}
    </div>
  );
}

function VoicePlayer({ voice, hideTranscript }) {
  const [playing, setPlaying] = useState(false);
  const [pos, setPos] = useState(0);
  const [audioError, setAudioError] = useState(null);
  const [analyser, setAnalyser] = useState(null);
  const rafRef = useRef(0);
  const startRef = useRef(0);
  const audioRef = useRef(null);
  const audioCtxRef = useRef(null);
  const srcNodeRef = useRef(null);
  const total = voice.duration;
  // Stable-random bar heights so they don't shimmer re-render (fallback bars)
  const bars = useMemo(
    () =>
      Array.from(
        { length: 48 },
        (_, i) =>
          0.2 + Math.abs(Math.sin(i * 0.9) * 0.6 + Math.sin(i * 0.31) * 0.35),
      ),
    [],
  );

  // Lazily wire a Web Audio analyser to the <audio> element on first play.
  const ensureAnalyser = () => {
    if (!voice.url || !audioRef.current || srcNodeRef.current) return;
    try {
      const Ctx = window.AudioContext || window.webkitAudioContext;
      const ctx = new Ctx();
      audioCtxRef.current = ctx;
      const src = ctx.createMediaElementSource(audioRef.current);
      const an = ctx.createAnalyser();
      an.fftSize = 256;
      an.smoothingTimeConstant = 0.8;
      src.connect(an);
      an.connect(ctx.destination);
      srcNodeRef.current = src;
      setAnalyser(an);
    } catch (e) {
      // CORS-tainted audio or unsupported — silently fall back to static bars
      console.warn("Playback analyser unavailable:", e?.message);
    }
  };

  useEffect(
    () => () => {
      if (audioCtxRef.current) audioCtxRef.current.close().catch(() => {});
    },
    [],
  );

  // Fake timer for feed posts (no url); real audio drives pos via timeupdate
  useEffect(() => {
    if (voice.url || !playing) return;
    startRef.current = performance.now() - pos * 1000;
    const tick = (t) => {
      const elapsed = (t - startRef.current) / 1000;
      if (elapsed >= total) {
        setPos(total);
        setPlaying(false);
        return;
      }
      setPos(elapsed);
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [playing]);

  const handlePlayPause = () => {
    if (voice.url && audioRef.current) {
      if (playing) {
        audioRef.current.pause();
        setPlaying(false);
      } else {
        if (pos >= total) {
          audioRef.current.currentTime = 0;
          setPos(0);
        }
        setAudioError(null);
        ensureAnalyser();
        if (audioCtxRef.current?.state === "suspended")
          audioCtxRef.current.resume().catch(() => {});
        audioRef.current
          .play()
          .then(() => setPlaying(true))
          .catch((err) => {
            console.error("Audio playback failed:", err);
            setAudioError(err.message || "Playback failed");
            setPlaying(false);
          });
      }
    } else {
      if (pos >= total) setPos(0);
      setPlaying((p) => !p);
    }
  };

  const progress = total ? pos / total : 0;
  const mins = Math.floor(pos / 60);
  const secs = Math.floor(pos % 60)
    .toString()
    .padStart(2, "0");
  const totalMins = Math.floor(total / 60);
  const totalSecs = Math.floor(total % 60)
    .toString()
    .padStart(2, "0");

  return (
    <div>
      {voice.url && (
        <audio
          ref={audioRef}
          src={voice.url}
          crossOrigin="anonymous"
          preload="auto"
          onTimeUpdate={() => setPos(audioRef.current?.currentTime ?? 0)}
          onEnded={() => {
            setPlaying(false);
            setPos(total);
          }}
          onError={(e) => {
            const err = e.currentTarget.error;
            const msg = err
              ? `MediaError code ${err.code}`
              : "Audio load error";
            console.error("Audio element error:", msg, e.currentTarget.src);
            setAudioError(msg);
          }}
        />
      )}
      {audioError && (
        <div style={{ fontSize: 11, color: "var(--danger)", marginBottom: 4 }}>
          ⚠ {audioError}
        </div>
      )}
      <div className="voice-player">
        <button
          className="voice-play"
          onClick={handlePlayPause}
          aria-label={playing ? "Pause" : "Play"}
        >
          {playing ? (
            <Icon.Pause style={{ width: 13, height: 13 }} />
          ) : (
            <Icon.Play style={{ width: 13, height: 13, marginLeft: 2 }} />
          )}
        </button>
        <div className="voice-wave-live">
          {/* Sleek wave: reacts to audio while playing, gentle idle otherwise.
              Falls back to static progress bars only if Web Audio is unavailable. */}
          {window.VoiceWave ? (
            <window.VoiceWave
              recording={playing}
              recorded={!playing}
              analyser={playing ? analyser : null}
            />
          ) : (
            bars.map((h, i) => (
              <span
                key={i}
                className={i / bars.length < progress ? "on" : ""}
                style={{ height: `${h * 100}%` }}
              />
            ))
          )}
        </div>
        <span className="voice-tag">
          Voice · {totalMins}:{totalSecs}
        </span>
        <div className="voice-meta">
          {mins}:{secs} / {totalMins}:{totalSecs}
        </div>
      </div>
      {!hideTranscript && voice.transcript && (
        <div className="voice-transcript">"{voice.transcript}"</div>
      )}
    </div>
  );
}

// ── Tag pill helper (shared) ────────────────────────────
function tagNode(t) {
  if (t === "Decision")
    return (
      <span className="tag decision">
        <Icon.Decision />
        Decision
      </span>
    );
  if (t === "Blocker")
    return (
      <span className="tag blocker">
        <Icon.Blocker />
        Blocker
      </span>
    );
  if (t === "Knowledge")
    return (
      <span className="tag knowledge">
        <Icon.Wiki />
        Knowledge
      </span>
    );
  if (t === "Experiment")
    return (
      <span className="tag experiment">
        <Icon.Spark />
        Experiment
      </span>
    );
  if (t === "Idea")
    return (
      <span className="tag idea">
        <Icon.Idea />
        Idea
      </span>
    );
  if (t === "Outcome")
    return (
      <span className="tag outcome">
        <Icon.Check />
        Outcome
      </span>
    );
  if (t === "Voice")
    return (
      <span className="tag voice">
        <Icon.Mic />
        Voice
      </span>
    );
  if (t === "Meeting")
    return (
      <span className="tag meeting">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          style={{ width: 12, height: 12 }}
        >
          <path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
          <circle cx="9" cy="7" r="4" />
          <path d="M23 21v-2a4 4 0 00-3-3.87" />
          <path d="M16 3.13a4 4 0 010 7.75" />
        </svg>
        Meeting
      </span>
    );
  return <span className="tag ghost">{t}</span>;
}

// Map a signal kind → the {pill label, css class, fan-out flow text} used by the
// .ai-preview rows, so the detail modal matches the post-transcription check-in screen.
const SIGNAL_META = {
  decision: { t: "DEC", css: "dec", flow: "Added to Decision Log" },
  blocker: { t: "BLK", css: "blk", flow: "Posted to the team's blocker board" },
  task: { t: "TASK", css: "tsk", flow: "Added to your to-do list" },
  delegation: { t: "TASK", css: "tsk", flow: "Sent to a teammate" },
  knowledge: { t: "KB", css: "kb", flow: "Added to Knowledge Base" },
  experiment: { t: "EXP", css: "exp", flow: "Added to Experiments" },
  idea: { t: "IDEA", css: "idea", flow: "Saved to your ideas backlog" },
  outcome: { t: "OUT", css: "out", flow: "Logged as an outcome" },
};
function signalMeta(kind) {
  return (
    SIGNAL_META[kind] || {
      t: (kind || "?").slice(0, 4).toUpperCase(),
      css: "tsk",
      flow: "",
    }
  );
}

// Fixed display order for signal groups
const SIGNAL_GROUP_ORDER = [
  "blocker",
  "decision",
  "task",
  "delegation",
  "outcome",
  "knowledge",
  "experiment",
  "idea",
];
const SIGNAL_GROUP_LABELS = {
  blocker: "Blockers",
  decision: "Decisions",
  task: "Tasks",
  delegation: "Delegations",
  outcome: "Outcomes",
  knowledge: "Knowledge",
  experiment: "Experiments",
  idea: "Ideas",
};
const SIGNALS_PER_GROUP = 7;

function SignalFanOut({ aiRows }) {
  const totalSignals = aiRows.length;
  const [expandedGroups, setExpandedGroups] = useState({});

  const grouped = useMemo(() => {
    const groups = {};
    for (const r of aiRows) {
      const k = r.kind || "task";
      if (!groups[k]) groups[k] = [];
      groups[k].push(r);
    }
    for (const k of Object.keys(groups)) {
      groups[k].sort((a, b) => (b.importance || 3) - (a.importance || 3));
    }
    return groups;
  }, [aiRows]);

  const orderedKinds = SIGNAL_GROUP_ORDER.filter((k) => grouped[k]?.length);

  const toggleGroup = (kind) =>
    setExpandedGroups((prev) => ({ ...prev, [kind]: !prev[kind] }));

  return (
    <div className="ai-preview">
      <div className="ph">
        <Icon.Spark />
        This check-in fanned out to{" "}
        <span style={{ fontWeight: 600, color: "var(--ink-2)" }}>
          {totalSignals} signal{totalSignals !== 1 ? "s" : ""}
        </span>
      </div>
      {orderedKinds.map((kind, gi) => {
        const items = grouped[kind];
        const expanded = !!expandedGroups[kind];
        const m0 = signalMeta(kind);
        return (
          <div
            key={kind}
            style={{
              marginTop: gi > 0 ? 6 : 4,
              paddingTop: gi > 0 ? 6 : 0,
              borderTop: gi > 0 ? "1px solid var(--line)" : "none",
            }}
          >
            <div
              className="row"
              onClick={() => toggleGroup(kind)}
              style={{
                cursor: "pointer",
                userSelect: "none",
              }}
            >
              <span className={`t ${m0.css}`}>{m0.t}</span>
              <span
                style={{
                  fontSize: 12.5,
                  fontWeight: 600,
                  color: "var(--ink-2)",
                  flex: 1,
                }}
              >
                {SIGNAL_GROUP_LABELS[kind] || kind}
                <span
                  style={{
                    fontWeight: 400,
                    color: "var(--ink-4)",
                    marginLeft: 4,
                  }}
                >
                  ({items.length})
                </span>
              </span>
              <Icon.Chevron
                style={{
                  width: 12,
                  height: 12,
                  color: "var(--ink-4)",
                  transform: expanded ? "rotate(180deg)" : "none",
                  transition: "transform 150ms ease",
                }}
              />
            </div>
            {expanded &&
              items.map((r, i) => (
                <div
                  key={i}
                  style={{
                    display: "flex",
                    alignItems: "baseline",
                    gap: 8,
                    padding: "3px 0 3px 22px",
                  }}
                >
                  <span
                    style={{
                      width: 5,
                      height: 5,
                      borderRadius: "50%",
                      background: "var(--ink-4)",
                      flexShrink: 0,
                      marginTop: 5,
                    }}
                  />
                  <span style={{ fontSize: 13, color: "var(--ink-1)" }}>
                    {r.text}
                  </span>
                </div>
              ))}
          </div>
        );
      })}
    </div>
  );
}

// Colour map for signal highlights in transcript text
const SIGNAL_COLORS = {
  decision: { bg: "var(--accent-soft)", border: "var(--accent)" },
  blocker: { bg: "var(--danger-soft)", border: "var(--danger)" },
  task: { bg: "var(--warn-soft)", border: "var(--warn)" },
  delegation: { bg: "var(--warn-soft)", border: "var(--warn)" },
  knowledge: { bg: "var(--ok-soft)", border: "var(--ok)" },
  experiment: { bg: "var(--experiment-soft)", border: "var(--experiment)" },
  idea: {
    bg: "var(--idea-soft, var(--accent-soft))",
    border: "var(--idea-alt, var(--accent))",
  },
  outcome: { bg: "var(--ok-soft)", border: "var(--ok)" },
};

function HighlightedTranscript({ text, aiRows }) {
  if (!text || !aiRows || !aiRows.length) return `"${text || ""}"`;
  // Build segments to highlight (longest match first to avoid overlaps)
  // Use quote (exact transcript substring) if available, fall back to text
  const highlights = aiRows
    .map((r) => ({ text: r.quote || r.text, kind: r.kind }))
    .filter((r) => r.text && r.text.length > 5)
    .sort((a, b) => b.text.length - a.text.length);
  if (!highlights.length) return `"${text}"`;

  // Find non-overlapping match positions
  const matches = [];
  const used = new Set();
  for (const h of highlights) {
    const idx = text.toLowerCase().indexOf(h.text.toLowerCase());
    if (idx === -1) continue;
    const end = idx + h.text.length;
    let overlaps = false;
    for (const m of matches) {
      if (idx < m.end && end > m.start) {
        overlaps = true;
        break;
      }
    }
    if (!overlaps) {
      matches.push({ start: idx, end, kind: h.kind });
    }
  }
  if (!matches.length) return `"${text}"`;
  matches.sort((a, b) => a.start - b.start);

  // Render with highlights
  const parts = [];
  let cursor = 0;
  parts.push('"');
  for (const m of matches) {
    if (m.start > cursor) parts.push(text.slice(cursor, m.start));
    const colors = SIGNAL_COLORS[m.kind] || {
      bg: "var(--line)",
      border: "var(--ink-4)",
    };
    parts.push(
      React.createElement(
        "mark",
        {
          key: m.start,
          style: {
            background: colors.bg,
            borderLeft: `3px solid ${colors.border}`,
            paddingLeft: 4,
            borderRadius: 2,
          },
        },
        text.slice(m.start, m.end),
      ),
    );
    cursor = m.end;
  }
  if (cursor < text.length) parts.push(text.slice(cursor));
  parts.push('"');
  return React.createElement(React.Fragment, null, ...parts);
}

// ── Update card (clickable summary → opens detail modal) ─
function UpdateCard({ u, isNew, authToken, onUpdate, onOpen }) {
  return (
    <article
      className={`update ${isNew ? "fan-new" : ""}`}
      onClick={onOpen}
      style={{ cursor: "pointer" }}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => {
        if (e.key === "Enter") onOpen?.();
      }}
    >
      <div className="update-head">
        <Avatar name={u.who} you={u.you} />
        <div className="meta">
          <div className="who">
            {u.who}
            {u.you && (
              <span
                style={{
                  color: "var(--ink-3)",
                  fontWeight: 400,
                  fontSize: 12,
                  marginLeft: 6,
                }}
              >
                (you)
              </span>
            )}
          </div>
          <div className="role">{u.role}</div>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          {u.tags?.map((t, i) => (
            <span key={i}>{tagNode(t)}</span>
          ))}
          <span className="time">{u.time}</span>
        </div>
      </div>

      {u.title && <div className="update-title">{u.title}</div>}
      {u.body && <div className="update-body">{u.body}</div>}
      {/* Stop card-click bubbling so play/pause doesn't open the modal */}
      {u.voice && (
        <div onClick={(e) => e.stopPropagation()}>
          <VoicePlayer voice={u.voice} />
        </div>
      )}
      {u.delegatedTo && (
        <div className="update-foot">
          <span className="link-pill">
            <Icon.Send style={{ width: 13, height: 13 }} /> Delegated to{" "}
            {u.delegatedTo}
          </span>
        </div>
      )}
    </article>
  );
}

// ── Updates view ────────────────────────────────────────
const ROLE_FILTER_OPTIONS = [
  "all",
  "Developer",
  "Designer",
  "Project Management",
];

function UpdatesView({
  updates,
  setUpdates,
  scope,
  squad,
  todaysCheckedIn,
  filter,
  setFilter,
  roleFilter,
  setRoleFilter,
  highlightIds,
  authToken,
  activeSquadId,
  orgs,
  userSquads,
  periodType,
  setPeriodType,
  periodOffset,
  setPeriodOffset,
  period,
}) {
  const { SQUAD } = window.SEED; // fallback only
  const members = squad && squad.length ? squad : SQUAD;

  const handleUpdateCard = (id, fields) => {
    if (setUpdates) {
      setUpdates((us) =>
        us.map((u) => (u.id === id ? { ...u, ...fields } : u)),
      );
    }
  };

  const handleDeleteCard = (id) => {
    if (setUpdates) setUpdates((us) => us.filter((u) => u.id !== id));
  };

  // Which check-in is open in the detail modal (or null)
  const [openCheckIn, setOpenCheckIn] = useState(null);

  const filtered = useMemo(() => {
    if (filter === "all") return updates;
    const map = {
      decisions: "Decision",
      blockers: "Blocker",
      ideas: "Idea",
      voice: "Voice",
      outcomes: "Outcome",
    };
    return updates.filter((u) => u.tags?.includes(map[filter]));
  }, [updates, filter]);

  const counts = useMemo(
    () => ({
      all: updates.length,
      decisions: updates.filter((u) => u.tags?.includes("Decision")).length,
      blockers: updates.filter((u) => u.tags?.includes("Blocker")).length,
      ideas: updates.filter((u) => u.tags?.includes("Idea")).length,
      voice: updates.filter((u) => u.tags?.includes("Voice")).length,
      outcomes: updates.filter((u) => u.tags?.includes("Outcome")).length,
    }),
    [updates],
  );

  // Standardised header title: "My {Level} — {Org-level name}".
  // The org-level name is resolved by walking the org tree up from the active
  // squad to the node matching the current scope's type.
  const SCOPE_LABEL = {
    squad: "My Squad",
    team: "My Team",
    group: "My Group",
    function: "My Function",
    business: "Business",
  };
  const headerTitle = useMemo(() => {
    const label = SCOPE_LABEL[scope] || "Pulse";
    const orgList = orgs && orgs.length ? orgs : [];
    const byId = Object.fromEntries(orgList.map((o) => [o.id, o]));
    // Walk up from the active squad to find the node of the right type.
    let node = byId[activeSquadId];
    const wantType = scope === "business" ? "business" : scope;
    let name = null;
    while (node) {
      if (node.type === wantType) {
        name = node.name;
        break;
      }
      node = node.parent_id ? byId[node.parent_id] : null;
    }
    // Fallbacks: squad name from userSquads, else nothing.
    if (!name && scope === "squad") {
      name =
        (userSquads || []).find((s) => s.id === activeSquadId)?.name || null;
    }
    return name ? `${label} — ${name}` : label;
  }, [scope, activeSquadId, orgs, userSquads]);

  const _tz =
    window._pulseTz || Intl.DateTimeFormat().resolvedOptions().timeZone;

  // Show the most recent check-in: who, when
  const latestLabel = useMemo(() => {
    const latest = updates.find((u) => u.createdAt);
    if (!latest) return null;

    const who = latest.you ? "you" : latest.who;
    const dt = new Date(latest.createdAt);
    const today = new Date();
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
    const timeStr = dt
      .toLocaleTimeString("en-GB", {
        hour: "numeric",
        minute: "2-digit",
        hour12: true,
        timeZone: _tz,
      })
      .toLowerCase();
    let dateStr;
    if (dt.toDateString() === today.toDateString()) {
      dateStr = "today";
    } else if (dt.toDateString() === yesterday.toDateString()) {
      dateStr = "yesterday";
    } else {
      dateStr = dt.toLocaleDateString("en-GB", {
        day: "numeric",
        month: "short",
        timeZone: _tz,
      });
    }
    return `${who} · ${dateStr}, ${timeStr}`;
  }, [updates]);

  return (
    <>
      <div className="main-head">
        <div className="main-title">{headerTitle}</div>
        <div className="period-types" role="tablist" aria-label="Time range">
          {window.PERIOD_TYPES.map((t) => (
            <button
              key={t}
              role="tab"
              aria-selected={periodType === t}
              className={`period-type${periodType === t ? " active" : ""}`}
              onClick={() => {
                setPeriodType(t);
                setPeriodOffset(0);
              }}
            >
              {t.charAt(0).toUpperCase() + t.slice(1)}
            </button>
          ))}
        </div>
      </div>

      <div className="period-nav">
        <button
          className="period-arrow"
          aria-label="Previous period"
          onClick={() => setPeriodOffset((o) => o - 1)}
        >
          ‹
        </button>
        <span className="period-label">
          {period.label}
          {period.label !== period.exact && (
            <span className="period-exact"> · {period.exact}</span>
          )}
        </span>
        <button
          className="period-arrow"
          aria-label="Next period"
          disabled={periodOffset >= 0}
          onClick={() => setPeriodOffset((o) => Math.min(0, o + 1))}
        >
          ›
        </button>
        {periodOffset !== 0 && (
          <button className="period-today" onClick={() => setPeriodOffset(0)}>
            Jump to current
          </button>
        )}
        {latestLabel && (
          <span className="period-meta">
            Latest ·{" "}
            <span style={{ color: "var(--ink-2)" }}>{latestLabel}</span>
          </span>
        )}
      </div>

      {/* AI summary */}
      <AISummaryCard
        authToken={authToken}
        scope={scope}
        activeSquadId={activeSquadId}
        roleFilter={roleFilter}
        period={period}
      />

      {/* Today's check-ins */}
      <div className="card checkins-card checkins-compact">
        {(() => {
          const pct = members.length
            ? Math.round((todaysCheckedIn / members.length) * 100)
            : 0;
          const r = 15;
          const stroke = 4;
          const svgSize = 38;
          const center = svgSize / 2;
          const circ = 2 * Math.PI * r;
          const dashoffset = circ - (pct / 100) * circ;
          const done = members.filter((m) => m.checkedIn);
          const notDone = members.filter((m) => !m.checkedIn);
          return (
            <div className="checkins-row">
              <span className="checkins-title">Today's check-ins</span>

              <div className="checkins-clusters">
                {done.length > 0 && (
                  <div
                    className="checkins-cluster done"
                    data-tooltip={
                      "Checked in today:\n" +
                      done
                        .map((m) => "- " + (m.you ? "You" : m.name))
                        .join("\n")
                    }
                  >
                    {done.slice(0, 3).map((m, i) => (
                      <div
                        key={m.id}
                        className="checkins-avatar-wrap"
                        style={{ zIndex: 10 - i }}
                      >
                        <Avatar name={m.name} size="sm" you={m.you} />
                      </div>
                    ))}
                    {done.length > 3 && (
                      <span className="checkins-cluster-more">
                        +{done.length - 3}
                      </span>
                    )}
                    <span className="checkins-cluster-tick">
                      <Icon.Check />
                    </span>
                  </div>
                )}
                {notDone.length > 0 && (
                  <div
                    className="checkins-cluster pending"
                    data-tooltip={
                      "Not yet checked in:\n" +
                      notDone
                        .map((m) => "- " + (m.you ? "You" : m.name))
                        .join("\n")
                    }
                  >
                    {notDone.slice(0, 3).map((m, i) => (
                      <div
                        key={m.id}
                        className="checkins-avatar-wrap"
                        style={{ zIndex: 10 - i }}
                      >
                        <Avatar name={m.name} size="sm" you={m.you} />
                      </div>
                    ))}
                    {notDone.length > 3 && (
                      <span className="checkins-cluster-more">
                        +{notDone.length - 3}
                      </span>
                    )}
                  </div>
                )}
              </div>

              <div
                data-tooltip={`${pct}% — ${todaysCheckedIn} of ${members.length} checked in`}
                style={{
                  width: svgSize,
                  height: svgSize,
                  flexShrink: 0,
                  position: "relative",
                }}
              >
                <svg
                  className="checkins-ring"
                  viewBox={`0 0 ${svgSize} ${svgSize}`}
                >
                  <circle
                    cx={center}
                    cy={center}
                    r={r}
                    fill="none"
                    stroke="rgba(31,26,46,0.12)"
                    strokeWidth={stroke}
                  />
                  <circle
                    cx={center}
                    cy={center}
                    r={r}
                    fill="none"
                    stroke="var(--ok)"
                    strokeWidth={stroke}
                    strokeLinecap="round"
                    strokeDasharray={circ}
                    strokeDashoffset={dashoffset}
                    transform={`rotate(-90 ${center} ${center})`}
                    style={{ transition: "stroke-dashoffset 600ms ease" }}
                  />
                </svg>
                <span
                  style={{
                    position: "absolute",
                    inset: 0,
                    display: "grid",
                    placeItems: "center",
                    fontSize: 10,
                    fontWeight: 700,
                    color: "var(--ink)",
                    fontVariantNumeric: "tabular-nums",
                  }}
                >
                  {todaysCheckedIn}/{members.length}
                </span>
              </div>
            </div>
          );
        })()}
      </div>

      {/* Filters */}
      <div className="chips">
        {[
          ["all", "All", Icon.Spark],
          ["decisions", "Decisions", Icon.Decision],
          ["blockers", "Blockers", Icon.Blocker],
          ["ideas", "Ideas", Icon.Idea],
          ["voice", "Voice", Icon.Mic],
          ["outcomes", "Outcomes", Icon.Check],
        ].map(([id, label, I]) => (
          <button
            key={id}
            className="chip"
            aria-pressed={filter === id}
            onClick={() => setFilter(id)}
          >
            <I /> {label} <span className="count">{counts[id]}</span>
          </button>
        ))}
        {setRoleFilter && (
          <label
            style={{
              display: "flex",
              alignItems: "center",
              gap: 6,
              fontSize: 12,
              color: "var(--ink-3)",
              marginLeft: "auto",
            }}
          >
            Filter by
            <select
              value={roleFilter}
              onChange={(e) => setRoleFilter(e.target.value)}
              style={{
                fontSize: 12,
                padding: "4px 8px",
                border: "1px solid var(--line)",
                borderRadius: 6,
                background: "var(--surface)",
                color: "var(--ink-1)",
              }}
            >
              {ROLE_FILTER_OPTIONS.map((r) => (
                <option key={r} value={r}>
                  {r === "all" ? "All roles" : r}
                </option>
              ))}
            </select>
          </label>
        )}
      </div>

      {/* Updates feed */}
      {filtered.map((u) => (
        <UpdateCard
          key={u.id}
          u={u}
          isNew={highlightIds.includes(u.id)}
          authToken={authToken}
          onUpdate={handleUpdateCard}
          onOpen={() => setOpenCheckIn(u)}
        />
      ))}

      {filtered.length === 0 && (
        <div
          className="card"
          style={{
            textAlign: "center",
            color: "var(--ink-3)",
            padding: "32px 18px",
          }}
        >
          Nothing here yet. Try another filter.
        </div>
      )}

      {openCheckIn && (
        <CheckInDetailModal
          u={openCheckIn}
          authToken={authToken}
          onClose={() => setOpenCheckIn(null)}
          onUpdate={(id, fields) => {
            handleUpdateCard(id, fields);
            setOpenCheckIn((o) => (o && o.id === id ? { ...o, ...fields } : o));
          }}
          onDelete={(id) => {
            handleDeleteCard(id);
            setOpenCheckIn(null);
          }}
        />
      )}
    </>
  );
}

// ── Check-in detail modal (view; editable + deletable if owner) ─────────────
const DETAIL_TAG_OPTIONS = ["decision", "blocker", "idea", "outcome"];

function CheckInDetailModal({ u, authToken, onClose, onUpdate, onDelete }) {
  const apiBase = window.PULSE_CONFIG?.apiBase || "http://localhost:8000";
  const isOwner = !!u.you;
  const [editing, setEditing] = useState(false);
  const [editBody, setEditBody] = useState(u.body || u.voice?.transcript || "");
  const [editTags, setEditTags] = useState(
    (u.aiRows || [])
      .filter((r) => DETAIL_TAG_OPTIONS.includes(r.kind))
      .map((r) => r.kind),
  );
  const [saving, setSaving] = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [deleting, setDeleting] = useState(false);
  const [error, setError] = useState(null);
  const [showFullTranscript, setShowFullTranscript] = useState(false);
  const isMeeting = u.tags?.includes("Meeting");

  // Esc closes the modal (but not mid-edit or mid-delete-confirm, to avoid losing input)
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape" && !editing && !confirmDelete) {
        e.stopPropagation();
        onClose();
      }
    };
    window.addEventListener("keydown", onKey, true);
    return () => window.removeEventListener("keydown", onKey, true);
  }, [editing, confirmDelete, onClose]);

  const toggleTag = (kind) =>
    setEditTags((ts) =>
      ts.includes(kind) ? ts.filter((t) => t !== kind) : [...ts, kind],
    );

  const save = async () => {
    setSaving(true);
    setError(null);
    const signals = editTags.map((kind) => {
      const existing = (u.aiRows || []).find((r) => r.kind === kind);
      return { kind, body: existing ? existing.text : editBody.slice(0, 120) };
    });
    try {
      const res = await fetch(`${apiBase}/check-ins/${u.id}`, {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${authToken}`,
        },
        body: JSON.stringify({ transcript: editBody, signals }),
      });
      if (!res.ok) throw new Error("Save failed");
      const data = await res.json();
      const kinds = (data.signals || []).map((s) => s.kind);
      const newTags = [];
      if (kinds.includes("decision")) newTags.push("Decision");
      if (kinds.includes("blocker")) newTags.push("Blocker");
      if (kinds.includes("idea")) newTags.push("Idea");
      if (kinds.includes("outcome")) newTags.push("Outcome");
      if (u.voice) newTags.push("Voice");
      onUpdate?.(u.id, {
        body: u.voice ? null : editBody,
        tags: newTags,
        aiRows: (data.signals || []).map((s) => ({
          kind: s.kind,
          text: s.body,
        })),
        voice: u.voice ? { ...u.voice, transcript: editBody } : null,
      });
      setEditing(false);
    } catch (e) {
      setError(e.message || "Save failed");
    }
    setSaving(false);
  };

  const doDelete = async () => {
    setDeleting(true);
    setError(null);
    try {
      const res = await fetch(`${apiBase}/check-ins/${u.id}`, {
        method: "DELETE",
        headers: { Authorization: `Bearer ${authToken}` },
      });
      if (!res.ok) throw new Error("Delete failed");
      onDelete?.(u.id);
    } catch (e) {
      setError(e.message || "Delete failed");
      setDeleting(false);
    }
  };

  return (
    <div
      className="modal-backdrop"
      onMouseDown={(e) => {
        if (e.target === e.currentTarget) onClose();
      }}
    >
      <div className="modal" role="dialog" aria-label="Check-in detail">
        <div className="modal-head">
          <div className="t">
            <Avatar name={u.who} you={u.you} size="sm" />
            <span style={{ marginLeft: 8 }}>
              {u.who}
              {u.you && " (you)"}
            </span>
            <span className="when">{u.time}</span>
          </div>
          <button className="modal-close" onClick={onClose} aria-label="Close">
            <Icon.Close />
          </button>
        </div>

        <div className="modal-body">
          {u.voice && (
            <div style={{ marginBottom: 14 }}>
              <VoicePlayer voice={u.voice} hideTranscript />
            </div>
          )}

          {editing ? (
            <>
              <label
                style={{
                  fontSize: 11,
                  fontWeight: 600,
                  color: "var(--ink-4)",
                  display: "block",
                  marginBottom: 4,
                }}
              >
                Transcript
              </label>
              <textarea
                className="edit-checkin-textarea"
                value={editBody}
                onChange={(e) => setEditBody(e.target.value)}
                rows={5}
                style={{ width: "100%" }}
              />
              <div className="edit-checkin-tags" style={{ marginTop: 10 }}>
                {DETAIL_TAG_OPTIONS.map((kind) => (
                  <button
                    key={kind}
                    className={`edit-tag-btn ${editTags.includes(kind) ? "active" : ""}`}
                    onClick={() => toggleTag(kind)}
                  >
                    {kind.charAt(0).toUpperCase() + kind.slice(1)}
                  </button>
                ))}
              </div>
            </>
          ) : (
            /* Read-only view styled to match the post-transcription check-in screen */
            <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>

              {u.body || u.voice?.transcript ? (
                <div className="voice-transcript" style={{ marginBottom: 10 }}>
                  <HighlightedTranscript
                    text={
                      isMeeting && !showFullTranscript
                        ? u.body || u.voice?.transcript
                        : u.fullTranscript || u.body || u.voice?.transcript
                    }
                    aiRows={u.aiRows}
                  />
                  {isMeeting && u.fullTranscript && (
                    <button
                      onClick={() => setShowFullTranscript((v) => !v)}
                      style={{
                        display: "block",
                        marginTop: 10,
                        fontSize: 12,
                        color: "var(--accent)",
                        background: "none",
                        border: "none",
                        cursor: "pointer",
                        padding: 0,
                        fontWeight: 500,
                      }}
                    >
                      {showFullTranscript
                        ? "Hide full transcript"
                        : "Show full transcript"}
                    </button>
                  )}
                </div>
              ) : (
                <div
                  className="voice-transcript"
                  style={{
                    marginBottom: 10,
                    color: "var(--ink-3)",
                    fontStyle: "italic",
                  }}
                >
                  No transcript.
                </div>
              )}

              {u.aiRows && u.aiRows.length > 0 && (
                <SignalFanOut aiRows={u.aiRows} />
              )}
            </div>
          )}

          {error && (
            <div
              style={{ color: "var(--danger)", fontSize: 12.5, marginTop: 10 }}
            >
              ⚠ {error}
            </div>
          )}
        </div>

        {isOwner && (
          <div
            className="modal-foot"
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
              padding: "12px 20px",
              borderTop: "1px solid var(--line)",
            }}
          >
            {editing ? (
              <>
                <button className="btn accent" onClick={save} disabled={saving}>
                  {saving ? "Saving…" : "Save changes"}
                </button>
                <button
                  className="btn"
                  onClick={() => setEditing(false)}
                  disabled={saving}
                >
                  Cancel
                </button>
              </>
            ) : confirmDelete ? (
              <>
                <span style={{ fontSize: 13, color: "var(--ink-2)" }}>
                  Delete this check-in?
                </span>
                <button
                  className="btn"
                  style={{ color: "var(--danger)" }}
                  onClick={doDelete}
                  disabled={deleting}
                >
                  {deleting ? "Deleting…" : "Delete"}
                </button>
                <button
                  className="btn"
                  onClick={() => setConfirmDelete(false)}
                  disabled={deleting}
                >
                  Keep
                </button>
              </>
            ) : (
              <>
                <button className="btn accent" onClick={() => setEditing(true)}>
                  <Icon.Edit
                    style={{
                      width: 13,
                      height: 13,
                      marginRight: 4,
                      verticalAlign: -2,
                    }}
                  />
                  Edit
                </button>
                <button
                  className="btn"
                  style={{ color: "var(--danger)", marginLeft: "auto" }}
                  onClick={() => setConfirmDelete(true)}
                >
                  Delete
                </button>
              </>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

window.UpdatesView = UpdatesView;
window.UpdateCard = UpdateCard;
window.VoicePlayer = VoicePlayer;
window.CheckInDetailModal = CheckInDetailModal;
