const { useState, useEffect, useRef, useCallback } = React;

// ---------- design tokens ----------
const COLORS = {
  paper: "#FBF7F0",
  paperRaised: "#FFFFFF",
  panel: "#F5F0E6",
  ink: "#221E1A",
  inkSoft: "#736A5E",
  inkFaint: "#A79C8C",
  border: "#EDE6D8",
  plum: "#5B3A5F",
  plumSoft: "#F1E7F0",
  overdue: "#E15C4D",
  overdueSoft: "#FBE7E3",
  today: "#E8A23C",
  todaySoft: "#FBEFDC",
  upcoming: "#3E7CA6",
  upcomingSoft: "#E3EEF5",
  done: "#3F7D5C",
  doneSoft: "#E4F0E9",
  markerYellow: "#FFE070",
};

// ---------- minimal inline icon set (no external icon lib needed) ----------
function Icon({ children, size = 16, style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
      strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={style}>
      {children}
    </svg>
  );
}
const Mic = (p) => <Icon {...p}><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><line x1="12" y1="19" x2="12" y2="23" /><line x1="8" y1="23" x2="16" y2="23" /></Icon>;
const Square = (p) => <Icon {...p}><rect x="4" y="4" width="16" height="16" rx="2" /></Icon>;
const Check = (p) => <Icon {...p}><polyline points="20 6 9 17 4 12" /></Icon>;
const X = (p) => <Icon {...p}><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></Icon>;
const Calendar = (p) => <Icon {...p}><rect x="3" y="4" width="18" height="18" rx="2" /><line x1="16" y1="2" x2="16" y2="6" /><line x1="8" y1="2" x2="8" y2="6" /><line x1="3" y1="10" x2="21" y2="10" /></Icon>;
const ChevronRight = (p) => <Icon {...p}><polyline points="9 18 15 12 9 6" /></Icon>;
const ChevronLeft = (p) => <Icon {...p}><polyline points="15 18 9 12 15 6" /></Icon>;
const UserCircle = (p) => <Icon {...p}><circle cx="12" cy="8" r="4" /><path d="M4 20c0-4 4-6 8-6s8 2 8 6" /></Icon>;
const FileText = (p) => <Icon {...p}><path d="M6 2h9l5 5v15H6z" /><polyline points="15 2 15 7 20 7" /></Icon>;
const AlertTriangle = (p) => <Icon {...p}><path d="M12 3 2 20h20L12 3z" /><line x1="12" y1="10" x2="12" y2="14" /><line x1="12" y1="17" x2="12.01" y2="17" /></Icon>;
const Loader = (p) => <Icon {...p}><line x1="12" y1="2" x2="12" y2="6" /><line x1="12" y1="18" x2="12" y2="22" /><line x1="4.9" y1="4.9" x2="7.7" y2="7.7" /><line x1="16.3" y1="16.3" x2="19.1" y2="19.1" /><line x1="2" y1="12" x2="6" y2="12" /><line x1="18" y1="12" x2="22" y2="12" /><line x1="4.9" y1="19.1" x2="7.7" y2="16.3" /><line x1="16.3" y1="7.7" x2="19.1" y2="4.9" /></Icon>;
const CheckCircle2 = (p) => <Icon {...p}><circle cx="12" cy="12" r="10" /><polyline points="9 12 11 14 15 10" /></Icon>;
const PauseCircle = (p) => <Icon {...p}><circle cx="12" cy="12" r="10" /><line x1="10" y1="9" x2="10" y2="15" /><line x1="14" y1="9" x2="14" y2="15" /></Icon>;
const Sparkles = (p) => <Icon {...p}><path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5 18 18M18 6l-2.5 2.5M8.5 15.5 6 18" /></Icon>;
const ArrowRight = (p) => <Icon {...p}><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></Icon>;
const Type = (p) => <Icon {...p}><polyline points="4 7 4 4 20 4 20 7" /><line x1="9" y1="20" x2="15" y2="20" /><line x1="12" y1="4" x2="12" y2="20" /></Icon>;
const RotateCcw = (p) => <Icon {...p}><path d="M3 12a9 9 0 1 0 3-6.7L3 8" /><polyline points="3 3 3 8 8 8" /></Icon>;

// ---------- helpers ----------
function todayISO() { return new Date().toISOString().slice(0, 10); }
function fmtDate(iso) {
  if (!iso) return "No date";
  const d = new Date(iso + "T00:00:00");
  if (isNaN(d.getTime())) return iso;
  return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
}
function daysUntil(iso) {
  if (!iso) return null;
  const today = new Date(todayISO() + "T00:00:00");
  const d = new Date(iso + "T00:00:00");
  return Math.round((d - today) / 86400000);
}
function statusBucket(c) {
  if (["completed", "dismissed"].includes(c.status)) return c.status;
  const du = daysUntil(c.dueAt);
  if (du === null) return "upcoming";
  if (du < 0) return "overdue";
  if (du === 0) return "today";
  return "upcoming";
}
function uid() { return crypto.randomUUID ? crypto.randomUUID() : "id-" + Math.random().toString(36).slice(2); }

// Speech recognition restarts mid-recap on every phone/browser (silence timeout, forced
// session caps around ~60s, etc), and the new session sometimes re-hears the tail end of
// what was just said before it stopped. Rather than blindly concatenating, find the
// longest run of words at the end of `base` that reappears at the start of `addition`
// (case/punctuation-insensitive) and only append what's actually new.
function normWordForOverlap(w) {
  return w.toLowerCase().replace(/[.,!?।]+$/, "");
}
function mergeWithOverlap(base, addition) {
  const baseTrim = base.trim();
  const addTrim = addition.trim();
  if (!addTrim) return baseTrim;
  if (!baseTrim) return addTrim;
  const baseWords = baseTrim.split(/\s+/);
  const addWords = addTrim.split(/\s+/);
  const maxOverlap = Math.min(baseWords.length, addWords.length, 10);
  for (let k = maxOverlap; k >= 1; k--) {
    // a 1-word overlap is only trusted if the whole new chunk is that one word —
    // otherwise a coincidental shared word (like "the") could eat real new text
    if (k === 1 && addWords.length > 1) continue;
    const baseTail = baseWords.slice(-k).map(normWordForOverlap).join(" ");
    const addHead = addWords.slice(0, k).map(normWordForOverlap).join(" ");
    if (baseTail === addHead) {
      const remainder = addWords.slice(k).join(" ");
      return remainder ? `${baseTrim} ${remainder}` : baseTrim;
    }
  }
  return `${baseTrim} ${addTrim}`;
}
function activeTabFor(view) {
  if (view === "settings") return "settings";
  if (["history", "meetingDetail"].includes(view)) return "history";
  return "dashboard";
}

// ---------- auth ----------
let authToken = null; // current Firebase ID token, refreshed by onIdTokenChanged in Root
let firebaseConfigPromise = null;

function initFirebase() {
  if (!firebaseConfigPromise) {
    firebaseConfigPromise = fetch("/api/firebase-config").then((r) => r.json()).then((config) => {
      if (!config.apiKey) throw new Error("no-firebase-config");
      firebase.initializeApp(config);
      return config;
    });
  }
  return firebaseConfigPromise;
}

// wraps fetch with the Firebase auth header, and signs out on an expired/invalid session
async function apiFetch(url, opts = {}) {
  const headers = { ...(opts.headers || {}) };
  if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
  const r = await fetch(url, { ...opts, headers });
  if (r.status === 401 && window.firebase && firebase.auth().currentUser) {
    firebase.auth().signOut();
  }
  return r;
}

// ---------- API client ----------
const api = {
  async loadAll() { const r = await apiFetch("/api/data"); return r.json(); },
  async createMeeting(transcript) {
    const r = await apiFetch("/api/meetings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transcript }) });
    return r.json();
  },
  async extract(meetingId) {
    const r = await apiFetch(`/api/meetings/${meetingId}/extract`, { method: "POST" });
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || "Extraction failed"); }
    return r.json();
  },
  async confirmMeeting(meetingId, decisions, approvedCommitments) {
    const r = await apiFetch(`/api/meetings/${meetingId}/confirm`, {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ decisions, approvedCommitments }),
    });
    return r.json();
  },
  async updateCommitment(id, patch) {
    const r = await apiFetch(`/api/commitments/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch) });
    return r.json();
  },
  async saveSettings(settings) {
    const r = await apiFetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings) });
    return r.json();
  },
  async checkReminders(phoneNumbers, commitments) {
    const r = await apiFetch("/api/check-reminders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phoneNumbers, commitments }) });
    return { ok: r.ok };
  },
};

// ---------- small UI atoms ----------
function ConfidenceBadge({ level }) {
  const map = {
    high: { bg: COLORS.doneSoft, fg: COLORS.done, label: "High confidence" },
    medium: { bg: COLORS.todaySoft, fg: COLORS.today, label: "Medium confidence" },
    low: { bg: COLORS.overdueSoft, fg: COLORS.overdue, label: "Low confidence" },
  };
  const s = map[level] || map.medium;
  return <span style={{ background: s.bg, color: s.fg }} className="text-xs font-medium px-2 py-1 rounded-full whitespace-nowrap">{s.label}</span>;
}
function SourceQuote({ text }) {
  if (!text) return null;
  return (
    <div className="text-sm italic mt-2" style={{ color: COLORS.inkSoft }}>
      <span style={{ background: `linear-gradient(180deg, transparent 62%, ${COLORS.markerYellow} 62%)` }}>"{text}"</span>
    </div>
  );
}
function StatusPill({ status }) {
  const map = {
    active: { bg: COLORS.plumSoft, fg: COLORS.plum, label: "Active" },
    today: { bg: COLORS.todaySoft, fg: COLORS.today, label: "Due today" },
    overdue: { bg: COLORS.overdueSoft, fg: COLORS.overdue, label: "Overdue" },
    upcoming: { bg: COLORS.upcomingSoft, fg: COLORS.upcoming, label: "Upcoming" },
    completed: { bg: COLORS.doneSoft, fg: COLORS.done, label: "Completed" },
    dismissed: { bg: COLORS.border, fg: COLORS.inkSoft, label: "Dismissed" },
  };
  const s = map[status] || map.upcoming;
  return <span style={{ background: s.bg, color: s.fg }} className="text-xs font-semibold px-2.5 py-1 rounded-full whitespace-nowrap">{s.label}</span>;
}
function BackRow({ onBack, label = "Back" }) {
  return <button onClick={onBack} className="text-sm font-medium flex items-center gap-1" style={{ color: COLORS.inkSoft }}><ChevronLeft size={16} /> {label}</button>;
}
function EmptyState({ icon, title, body }) {
  return (
    <div className="text-center py-14 px-6 rounded-2xl border border-dashed" style={{ borderColor: COLORS.border }}>
      <div className="inline-flex items-center justify-center w-11 h-11 rounded-full mb-4" style={{ background: COLORS.plumSoft, color: COLORS.plum }}>{icon}</div>
      <div className="display text-lg font-medium mb-1">{title}</div>
      <div className="text-sm max-w-xs mx-auto" style={{ color: COLORS.inkSoft }}>{body}</div>
    </div>
  );
}

// ---------- login ----------
function LoginScreen() {
  const [phone, setPhone] = useState("");
  const [code, setCode] = useState("");
  const [step, setStep] = useState("phone"); // "phone" | "code"
  const [sending, setSending] = useState(false);
  const [error, setError] = useState(null);
  const confirmationRef = useRef(null);
  const recaptchaRef = useRef(null);
  const widgetIdRef = useRef(null);
  const sendingRef = useRef(false); // synchronous guard — React state updates are too slow to stop a fast double-click

  useEffect(() => {
    if (!recaptchaRef.current) {
      recaptchaRef.current = new firebase.auth.RecaptchaVerifier("recaptcha-container", { size: "invisible" });
      // rendering once up front (rather than letting signInWithPhoneNumber render it lazily)
      // means a failed attempt can reset the same widget instead of re-rendering into the
      // same DOM node, which is what throws "reCAPTCHA has already been rendered in this element"
      recaptchaRef.current.render().then((widgetId) => { widgetIdRef.current = widgetId; });
    }
  }, []);

  async function sendCode() {
    if (sendingRef.current) return;
    setError(null);
    const digits = phone.trim();
    if (!/^\d{10}$/.test(digits)) {
      setError("Enter a 10 digit phone number");
      return;
    }
    const trimmed = `+91${digits}`;
    sendingRef.current = true;
    setSending(true);
    try {
      confirmationRef.current = await firebase.auth().signInWithPhoneNumber(trimmed, recaptchaRef.current);
      setStep("code");
    } catch (e) {
      setError(e.message || "Couldn't send the code — try again.");
      if (window.grecaptcha && widgetIdRef.current !== null) window.grecaptcha.reset(widgetIdRef.current);
    } finally {
      setSending(false);
      sendingRef.current = false;
    }
  }

  async function verifyCode() {
    setError(null);
    if (!code.trim()) return;
    setSending(true);
    try {
      await confirmationRef.current.confirm(code.trim());
      // onAuthStateChanged in Root picks this up and swaps to the app
    } catch (e) {
      setError("That code didn't match — try again.");
    } finally {
      setSending(false);
    }
  }

  return (
    <div style={{ background: COLORS.paper, minHeight: "100vh", fontFamily: "'IBM Plex Sans', sans-serif", color: COLORS.ink }}>
      <div style={{ maxWidth: 400, margin: "0 auto", padding: "80px 24px" }}>
        <div className="display text-2xl font-semibold tracking-tight mb-1" style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <img src="/favicon.svg" alt="" width={28} height={28} />
          Arjava Assistant
        </div>
        <div className="text-sm mb-8" style={{ color: COLORS.inkSoft }}>Sign in with your phone number to continue.</div>

        {error && <div className="p-3 rounded-lg text-sm mb-4" style={{ background: COLORS.overdueSoft, color: COLORS.overdue }}>{error}</div>}

        {step === "phone" && (
          <>
            <label className="text-xs font-semibold uppercase" style={{ color: COLORS.inkSoft }}>Phone number</label>
            <div className="flex items-stretch mt-1.5 mb-4">
              <span className="flex items-center px-3 rounded-l-lg border border-r-0 text-sm" style={{ borderColor: COLORS.border, color: COLORS.inkSoft }}>+91</span>
              <input value={phone} onChange={(e) => setPhone(e.target.value.replace(/\D/g, "").slice(0, 10))}
                placeholder="9876543210" inputMode="numeric" pattern="[0-9]*" maxLength={10}
                className="w-full p-3 rounded-r-lg border text-sm" style={{ borderColor: COLORS.border }} />
            </div>
            <button disabled={sending} onClick={sendCode} className="w-full py-3.5 rounded-xl font-medium text-white flex items-center justify-center gap-2"
              style={{ background: COLORS.plum, opacity: sending ? 0.6 : 1 }}>
              {sending ? <Loader size={16} className="spin" /> : null} Send code
            </button>
          </>
        )}

        {step === "code" && (
          <>
            <div className="text-sm mb-4" style={{ color: COLORS.inkSoft }}>Enter the code sent to +91{phone}.</div>
            <label className="text-xs font-semibold uppercase" style={{ color: COLORS.inkSoft }}>Verification code</label>
            <input value={code} onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
              placeholder="123456" inputMode="numeric" pattern="[0-9]*" maxLength={6}
              className="w-full p-3 rounded-lg border text-sm mt-1.5 mb-4" style={{ borderColor: COLORS.border }} />
            <button disabled={sending} onClick={verifyCode} className="w-full py-3.5 rounded-xl font-medium text-white flex items-center justify-center gap-2"
              style={{ background: COLORS.plum, opacity: sending ? 0.6 : 1 }}>
              {sending ? <Loader size={16} className="spin" /> : null} Verify & sign in
            </button>
            <button onClick={() => { setStep("phone"); setCode(""); setError(null); }} className="text-sm font-medium mt-4" style={{ color: COLORS.inkSoft }}>
              Use a different number
            </button>
          </>
        )}
      </div>
      <div id="recaptcha-container" />
    </div>
  );
}

// ---------- auth gate ----------
function Root() {
  const [phase, setPhase] = useState("loading"); // "loading" | "ready" | "config-error"
  const [user, setUser] = useState(null);

  useEffect(() => {
    initFirebase()
      .then(() => {
        firebase.auth().onIdTokenChanged(async (u) => {
          authToken = u ? await u.getIdToken() : null;
          setUser(u);
          setPhase("ready");
        });
      })
      .catch(() => setPhase("config-error"));
  }, []);

  if (phase === "config-error") {
    return (
      <div className="flex items-center justify-center" style={{ minHeight: "100vh", background: COLORS.paper, color: COLORS.inkSoft }}>
        <div className="text-center max-w-sm px-6 text-sm">Login isn't configured yet — add your Firebase project's config to .env on the server, then restart it.</div>
      </div>
    );
  }
  if (phase === "loading") {
    return <div className="flex items-center justify-center" style={{ minHeight: "100vh", background: COLORS.paper, color: COLORS.inkSoft }}><Loader size={18} className="spin" /></div>;
  }
  if (!user) return <LoginScreen />;
  return <App onLogout={() => firebase.auth().signOut()} />;
}

// ---------- main app ----------
function App({ onLogout }) {
  const [view, setView] = useState("dashboard");
  const [meetings, setMeetings] = useState([]);
  const [commitments, setCommitments] = useState([]);
  const [settings, setSettings] = useState({ whatsappEnabled: false, phoneNumbers: [] });
  const [loaded, setLoaded] = useState(false);
  const [error, setError] = useState(null);
  const [reminderStatus, setReminderStatus] = useState(null);

  const [liveTranscript, setLiveTranscript] = useState("");
  const [listening, setListening] = useState(false);
  const [typedFallback, setTypedFallback] = useState("");
  const [useTyped, setUseTyped] = useState(false);
  const recognitionRef = useRef(null);
  const transcriptRef = useRef(""); // finalized speech carried across auto-restarts
  const manualStopRef = useRef(false);
  const restartCountRef = useRef(0);
  const watchdogRef = useRef(null);
  const [speechSupported, setSpeechSupported] = useState(true);
  const [micError, setMicError] = useState(null);
  const [speechLang, setSpeechLang] = useState(() => localStorage.getItem("kept_speechLang") || "en-US");

  const [reviewData, setReviewData] = useState(null);
  const [selectedCommitmentId, setSelectedCommitmentId] = useState(null);
  const [selectedMeetingId, setSelectedMeetingId] = useState(null);

  useEffect(() => {
    (async () => {
      try {
        const data = await api.loadAll();
        setMeetings((data.meetings || []).sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || "")));
        setCommitments(data.commitments || []);
        setSettings((s) => ({ ...s, ...(data.settings || {}) }));
      } catch (e) {
        setError("Couldn't load your data — make sure the server is running, then refresh.");
      } finally {
        setLoaded(true);
      }
    })();
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) setSpeechSupported(false);
  }, []);

  // periodic check-in while the app is open — sends WhatsApp for anything due/overdue
  useEffect(() => {
    if (!settings.whatsappEnabled || !loaded) return;
    const dueNow = commitments.filter((c) => c.status === "active" && ["overdue", "today"].includes(statusBucket(c)));
    if (dueNow.length === 0) return;
    (async () => {
      setReminderStatus("sending");
      const res = await api.checkReminders(settings.phoneNumbers, dueNow);
      setReminderStatus(res.ok ? "sent" : "error");
    })();
  }, [loaded, settings.whatsappEnabled, (settings.phoneNumbers || []).join(","), commitments.length]);

  const FATAL_SPEECH_ERRORS = {
    "not-allowed": "Microphone access was blocked. Allow microphone access for this site in your browser settings, then try again.",
    "service-not-allowed": "Speech recognition isn't available right now. Try again in a moment, or type it instead.",
    "audio-capture": "No microphone was found. Check your mic is connected, then try again.",
    "language-not-supported": "This browser's speech recognition doesn't support the current language.",
  };

  const startListening = useCallback(() => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) { setUseTyped(true); return; }

    const beginSession = () => {
      const rec = new SR();
      rec.continuous = true;
      rec.interimResults = true;
      rec.lang = speechLang;
      rec.onresult = (e) => {
        clearTimeout(watchdogRef.current); // real audio is coming through — the engine is alive
        restartCountRef.current = 0; // real audio is coming through — reset the failed-restart guard
        let sessionFinal = "", sessionInterim = "", lastFinalNorm = "";
        for (let i = 0; i < e.results.length; i++) {
          const t = e.results[i][0].transcript;
          if (e.results[i].isFinal) {
            // some engines occasionally re-emit the same final result back to back
            // (e.g. Android re-confirming on a restart-prone connection) — skip an
            // exact repeat of the immediately preceding final chunk in this session.
            const norm = t.trim().toLowerCase();
            if (norm && norm === lastFinalNorm) continue;
            sessionFinal += t + " ";
            lastFinalNorm = norm;
          } else {
            sessionInterim += t + " ";
          }
        }
        rec._sessionFinal = sessionFinal;
        setLiveTranscript((transcriptRef.current + " " + sessionFinal + sessionInterim).trim());
      };
      rec.onerror = (e) => { rec._lastError = e.error; if (e.error) console.warn("[voice] recognition error:", e.error); };
      rec.onend = () => {
        clearTimeout(watchdogRef.current); // the engine called back, however it ended — it's not stuck
        const chunk = (rec._sessionFinal || "").trim();
        if (chunk) transcriptRef.current = mergeWithOverlap(transcriptRef.current, chunk);
        if (manualStopRef.current) {
          setListening(false);
          return;
        }
        const fatalMessage = FATAL_SPEECH_ERRORS[rec._lastError];
        if (fatalMessage) {
          setMicError(fatalMessage);
          setListening(false);
          return;
        }
        restartCountRef.current += 1;
        if (restartCountRef.current > 6) {
          // repeatedly failing to even start (e.g. flaky hardware) — stop looping silently
          const code = rec._lastError ? ` (${rec._lastError})` : "";
          setMicError(`Couldn't keep the microphone listening${code}. Try again, or type it instead.`);
          setListening(false);
          return;
        }
        // the speech engine stops itself after a pause of silence (or a transient error like
        // "no-speech"/"network") even with continuous=true — restart transparently so a natural
        // pause mid-recap doesn't truncate the recording. A short delay gives the OS/browser time
        // to release the microphone from the previous session before a new one grabs it — starting
        // immediately can make every restart fail instantly on some browsers.
        setTimeout(beginSession, 300);
      };
      recognitionRef.current = rec;

      const failToStart = (err) => {
        setMicError(`Couldn't start the microphone${err && err.name ? ` (${err.name})` : ""}. Try again, or type it instead.`);
        setListening(false);
      };
      try {
        rec.start();
      } catch (err) {
        // some engines briefly reject start() right after a previous session ends
        setTimeout(() => { try { rec.start(); } catch (e) { failToStart(e); } }, 150);
      }
      // some mobile browsers accept start() without throwing, then never call back at all —
      // no onresult, onerror, or onend — leaving the UI stuck showing "listening" forever with
      // no feedback. If nothing has happened by the time a real recognition session would have
      // reported silence, treat it as stuck rather than leave the user staring at a dead mic.
      clearTimeout(watchdogRef.current);
      watchdogRef.current = setTimeout(() => {
        if (manualStopRef.current) return;
        try { rec.abort(); } catch (e) {}
        setMicError("The microphone isn't responding. Try again, or type it instead.");
        setListening(false);
      }, 12000);
    };

    manualStopRef.current = false;
    restartCountRef.current = 0;
    transcriptRef.current = "";
    setMicError(null);
    setLiveTranscript("");
    beginSession();
    setListening(true);
  }, [speechLang]);

  function updateSpeechLang(lang) {
    setSpeechLang(lang);
    localStorage.setItem("kept_speechLang", lang);
  }
  const stopListening = useCallback(() => {
    manualStopRef.current = true;
    if (recognitionRef.current) recognitionRef.current.stop();
  }, []);

  async function processTranscript(transcript) {
    setView("processing");
    setError(null);
    try {
      const { meeting } = await api.createMeeting(transcript);
      setMeetings((prev) => [meeting, ...prev]);
      const result = await api.extract(meeting.id);
      setReviewData({
        meetingId: meeting.id,
        decisions: result.decisions || [],
        commitments: (result.commitments || []).map((c) => ({ ...c, _localId: uid(), _approved: null })),
        openQuestions: result.openQuestions || [],
      });
      setView("review");
    } catch (e) {
      setError(e.message || "Something went wrong while processing.");
      setView("dashboard");
    }
  }

  function updateReviewItem(localId, patch) {
    setReviewData((rd) => ({ ...rd, commitments: rd.commitments.map((c) => (c._localId === localId ? { ...c, ...patch } : c)) }));
  }

  async function finishReview() {
    const { meetingId, decisions, commitments: items } = reviewData;
    const approved = items.filter((c) => c._approved === true).map((c) => ({ ...c, owner: c.owner && c.owner.trim() ? c.owner : "Me" }));
    const { meeting, commitments: created } = await api.confirmMeeting(meetingId, decisions, approved);
    setMeetings((prev) => prev.map((m) => (m.id === meetingId ? meeting : m)));
    setCommitments((prev) => [...prev, ...created]);
    setReviewData(null);
    setLiveTranscript("");
    setTypedFallback("");
    setUseTyped(false);
    setView("dashboard");
  }

  async function updateCommitment(id, patch) {
    const { commitment } = await api.updateCommitment(id, patch);
    setCommitments((prev) => prev.map((c) => (c.id === id ? commitment : c)));
  }

  async function saveSettings(next) {
    const { settings: saved } = await api.saveSettings(next);
    setSettings(saved);
  }

  const grouped = {
    overdue: commitments.filter((c) => statusBucket(c) === "overdue"),
    today: commitments.filter((c) => statusBucket(c) === "today"),
    upcoming: commitments.filter((c) => statusBucket(c) === "upcoming"),
    completed: commitments.filter((c) => ["completed", "dismissed"].includes(c.status)),
  };
  const selectedCommitment = commitments.find((c) => c.id === selectedCommitmentId);
  const selectedMeeting = meetings.find((m) => m.id === selectedMeetingId);

  return (
    <div style={{ background: COLORS.paper, minHeight: "100vh", fontFamily: "'IBM Plex Sans', sans-serif", color: COLORS.ink }}>
      <style>{`
        .display { font-family: 'Fredoka', sans-serif; }
        .mono { font-family: 'IBM Plex Mono', monospace; }
        button { cursor: pointer; }
        input, textarea, select { font-family: inherit; }
        @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
        .spin { animation: spin 1s linear infinite; }
      `}</style>
      <div style={{ maxWidth: 640, margin: "0 auto", padding: "32px 20px 120px" }}>
        <Header whatsappEnabled={settings.whatsappEnabled} reminderStatus={reminderStatus} />

        {!loaded && <div className="flex items-center justify-center py-24" style={{ color: COLORS.inkSoft }}><Loader size={18} className="spin" style={{ marginRight: 8 }} /> Loading your workspace…</div>}

        {loaded && view === "dashboard" && (
          <Dashboard grouped={grouped} commitments={commitments} onSelectCommitment={(id) => { setSelectedCommitmentId(id); setView("detail"); }} onCompleteCommitment={(id) => updateCommitment(id, { status: "completed" })} onStartCapture={() => setView("capture")} error={error} reminderStatus={reminderStatus} />
        )}
        {view === "capture" && (
          <CaptureScreen listening={listening} liveTranscript={liveTranscript} setLiveTranscript={setLiveTranscript} useTyped={useTyped} setUseTyped={setUseTyped}
            typedFallback={typedFallback} setTypedFallback={setTypedFallback} speechSupported={speechSupported} micError={micError}
            speechLang={speechLang} onChangeSpeechLang={updateSpeechLang}
            onStart={startListening} onStop={stopListening} onSubmit={processTranscript}
            onBack={() => { stopListening(); setView("dashboard"); }} />
        )}
        {view === "processing" && <ProcessingScreen onBack={() => setView("dashboard")} />}
        {view === "review" && reviewData && <ReviewScreen data={reviewData} onUpdate={updateReviewItem} onFinish={finishReview} />}
        {view === "detail" && selectedCommitment && (
          <CommitmentDetail commitment={selectedCommitment} meeting={meetings.find((m) => m.id === selectedCommitment.meetingId)}
            onBack={() => setView("dashboard")} onUpdate={(patch) => updateCommitment(selectedCommitment.id, patch)} />
        )}
        {view === "settings" && <SettingsScreen settings={settings} onSave={saveSettings} onBack={() => setView("dashboard")} onLogout={onLogout} />}
        {view === "history" && (
          <MeetingHistory meetings={meetings} commitments={commitments}
            onOpen={(id) => { setSelectedMeetingId(id); setView("meetingDetail"); }} onBack={() => setView("dashboard")} />
        )}
        {view === "meetingDetail" && selectedMeeting && (
          <MeetingDetail meeting={selectedMeeting} commitments={commitments.filter((c) => c.meetingId === selectedMeeting.id)}
            onBack={() => setView("history")} onSelectCommitment={(id) => { setSelectedCommitmentId(id); setView("detail"); }} />
        )}
      </div>
      <BottomTabBar view={view} setView={setView} />
    </div>
  );
}

function Header({ whatsappEnabled, reminderStatus }) {
  return (
    <div className="mb-8">
      <div className="display text-2xl font-semibold tracking-tight" style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <img src="/favicon.svg" alt="" width={22} height={22} />
        Arjava Assistant
        {whatsappEnabled && <span title={reminderStatus === "error" ? "Reminder check failed" : "WhatsApp reminders on"} style={{ width: 7, height: 7, borderRadius: 999, background: reminderStatus === "error" ? COLORS.overdue : COLORS.done, display: "inline-block" }} />}
      </div>
      <div className="text-xs mono hidden sm:block" style={{ color: COLORS.inkSoft, whiteSpace: "nowrap" }}>said → confirmed → tracked</div>
    </div>
  );
}

function BottomTabBar({ view, setView }) {
  const active = activeTabFor(view);
  const tabs = [
    { key: "dashboard", label: "Today" },
    { key: "history", label: "History" },
    { key: "settings", label: "Settings" },
  ];
  return (
    <div style={{ position: "fixed", left: 0, right: 0, bottom: 0, display: "flex", justifyContent: "center", padding: "16px 20px", pointerEvents: "none" }}>
      <div className="flex items-center gap-1" style={{ background: COLORS.paperRaised, border: `1px solid ${COLORS.border}`, borderRadius: 20, padding: 6, boxShadow: "0 8px 24px rgba(34,30,26,0.10)", pointerEvents: "auto" }}>
        {tabs.map((t) => (
          <button key={t.key} onClick={() => setView(t.key)} className="text-xs font-semibold px-5 py-2.5 rounded-2xl"
            style={{ color: active === t.key ? COLORS.plum : COLORS.inkFaint, background: active === t.key ? COLORS.plumSoft : "transparent" }}>
            {t.label}
          </button>
        ))}
      </div>
    </div>
  );
}

function ProgressRing({ done, total }) {
  const pct = total > 0 ? done / total : 0;
  const deg = Math.round(pct * 360);
  return (
    <div className="p-5 rounded-2xl border flex items-center gap-5 mb-5" style={{ borderColor: COLORS.border, background: COLORS.paperRaised }}>
      <div style={{
        width: 92, height: 92, borderRadius: "50%", flexShrink: 0, position: "relative",
        background: total > 0 ? `conic-gradient(${COLORS.plum} 0deg ${deg}deg, ${COLORS.border} ${deg}deg 360deg)` : COLORS.border,
      }}>
        <div style={{ position: "absolute", inset: 9, borderRadius: "50%", background: COLORS.paperRaised, display: "flex", alignItems: "center", justifyContent: "center" }}>
          <div className="display" style={{ fontSize: 19, fontWeight: 600, lineHeight: 1 }}>{done}/{total}</div>
        </div>
      </div>
      <div>
        <div className="font-semibold text-sm">Today's progress</div>
        <div className="text-xs mt-1" style={{ color: COLORS.inkSoft }}>{total === 0 ? "Nothing due today" : `${done} of ${total} handled today`}</div>
      </div>
    </div>
  );
}

function StatTiles({ grouped }) {
  const tiles = [
    { key: "overdue", label: "OVERDUE", count: grouped.overdue.length, fg: COLORS.overdue, bg: COLORS.overdueSoft },
    { key: "today", label: "DUE TODAY", count: grouped.today.length, fg: COLORS.today, bg: COLORS.todaySoft },
    { key: "upcoming", label: "UPCOMING", count: grouped.upcoming.length, fg: COLORS.upcoming, bg: COLORS.upcomingSoft },
    { key: "completed", label: "COMPLETED", count: grouped.completed.length, fg: COLORS.done, bg: COLORS.doneSoft },
  ];
  return (
    <div className="grid grid-cols-2 gap-3 mb-8">
      {tiles.map((t) => (
        <div key={t.key} className="rounded-2xl px-4 py-3.5" style={{ background: t.bg }}>
          <div className="display" style={{ fontSize: 26, fontWeight: 600, color: COLORS.ink, lineHeight: 1 }}>{t.count}</div>
          <div className="text-xs font-semibold mt-1.5" style={{ color: t.fg, letterSpacing: "0.03em" }}>{t.label}</div>
        </div>
      ))}
    </div>
  );
}

function Dashboard({ grouped, commitments, onSelectCommitment, onCompleteCommitment, onStartCapture, error, reminderStatus }) {
  const totalActive = grouped.overdue.length + grouped.today.length + grouped.upcoming.length;
  const today = todayISO();
  const dueToday = commitments.filter((c) => c.dueAt === today);
  const dueTodayDone = dueToday.filter((c) => ["completed", "dismissed"].includes(c.status)).length;
  return (
    <div>
      {error && (
        <div className="p-3 rounded-lg text-sm mb-4" style={{ background: COLORS.overdueSoft, color: COLORS.overdue }}>{error}</div>
      )}
      {reminderStatus === "error" && (
        <div className="p-3 rounded-lg text-sm mb-4" style={{ background: COLORS.overdueSoft, color: COLORS.overdue }}>Couldn't send your WhatsApp reminder — check the number in Settings.</div>
      )}
      <button onClick={onStartCapture} className="w-full flex items-center justify-center gap-2 py-4 rounded-2xl mb-6 text-white font-medium shadow-sm" style={{ background: COLORS.plum }}>
        <Mic size={18} /> Capture a recap
      </button>
      {(totalActive > 0 || grouped.completed.length > 0) && (
        <>
          <ProgressRing done={dueTodayDone} total={dueToday.length} />
          <StatTiles grouped={grouped} />
        </>
      )}
      {totalActive === 0 && grouped.completed.length === 0 && (
        <EmptyState icon={<Sparkles size={22} />} title="Nothing tracked yet" body="Record a short recap of what you agreed on, and it'll show up here as commitments — not just notes." />
      )}
      {grouped.overdue.length > 0 && <Section title="Overdue" tint={COLORS.overdue} items={grouped.overdue} onSelect={onSelectCommitment} onComplete={onCompleteCommitment} />}
      {grouped.today.length > 0 && <Section title="Due today" tint={COLORS.today} items={grouped.today} onSelect={onSelectCommitment} onComplete={onCompleteCommitment} />}
      {grouped.upcoming.length > 0 && <Section title="Upcoming" tint={COLORS.upcoming} items={grouped.upcoming} onSelect={onSelectCommitment} onComplete={onCompleteCommitment} />}
      {grouped.completed.length > 0 && <Section title="Completed & closed" tint={COLORS.done} items={grouped.completed} onSelect={onSelectCommitment} muted collapsible defaultCollapsed />}
    </div>
  );
}

function Section({ title, tint, items, onSelect, onComplete, muted, collapsible, defaultCollapsed }) {
  const [collapsed, setCollapsed] = useState(!!defaultCollapsed);
  const open = !collapsible || !collapsed;
  return (
    <div className="mb-7">
      <div className="flex items-center gap-2 mb-3" onClick={collapsible ? () => setCollapsed((v) => !v) : undefined} style={{ cursor: collapsible ? "pointer" : "default" }}>
        <div style={{ width: 6, height: 6, borderRadius: 999, background: tint }} />
        <div className="text-sm font-semibold uppercase" style={{ color: COLORS.inkSoft, letterSpacing: "0.05em" }}>{title}</div>
        <div className="text-sm mono" style={{ color: COLORS.inkSoft }}>({items.length})</div>
        {collapsible && <ChevronRight size={14} style={{ color: COLORS.inkSoft, transform: open ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.15s" }} />}
      </div>
      {open && <div className="flex flex-col gap-2">
        {items.map((c) => (
          <div key={c.id} className="p-4 rounded-xl border flex items-center gap-3" style={{ borderColor: COLORS.border, background: COLORS.paperRaised, opacity: muted ? 0.7 : 1 }}>
            {onComplete && (
              <button onClick={() => onComplete(c.id)} title="Mark done" aria-label="Mark done" className="flex-shrink-0" style={{ color: COLORS.border }}>
                <CheckCircle2 size={22} />
              </button>
            )}
            <button onClick={() => onSelect(c.id)} className="text-left flex-1 flex items-center justify-between gap-3" style={{ minWidth: 0 }}>
              <div style={{ minWidth: 0 }}>
                <div className="font-medium" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{c.description}</div>
                <div className="text-sm mt-1 flex items-center gap-3" style={{ color: COLORS.inkSoft }}>
                  {c.owner && <span className="flex items-center gap-1"><UserCircle size={14} />{c.owner}</span>}
                  {c.dueAt && <span className="flex items-center gap-1"><Calendar size={14} />{fmtDate(c.dueAt)}</span>}
                </div>
              </div>
              <ChevronRight size={18} style={{ color: COLORS.inkSoft, flexShrink: 0 }} />
            </button>
          </div>
        ))}
      </div>}
    </div>
  );
}

function LangToggle({ value, onChange, disabled }) {
  const langs = [
    { code: "en-US", label: "English" },
    { code: "hi-IN", label: "हिंदी" },
  ];
  return (
    <div className="flex items-center gap-1 mb-4" style={{ background: COLORS.panel, borderRadius: 999, padding: 4 }}>
      {langs.map((l) => (
        <button key={l.code} disabled={disabled} onClick={() => onChange(l.code)} className="text-xs font-semibold px-3.5 py-1.5 rounded-full"
          style={{ color: value === l.code ? "white" : COLORS.inkSoft, background: value === l.code ? COLORS.plum : "transparent", opacity: disabled ? 0.6 : 1 }}>
          {l.label}
        </button>
      ))}
    </div>
  );
}

function CaptureScreen({ listening, liveTranscript, setLiveTranscript, useTyped, setUseTyped, typedFallback, setTypedFallback, speechSupported, micError, speechLang, onChangeSpeechLang, onStart, onStop, onSubmit, onBack }) {
  const text = useTyped ? typedFallback : liveTranscript;
  return (
    <div>
      <BackRow onBack={onBack} label="Cancel" />
      <div className="display text-xl font-medium mb-1 mt-4">Post-meeting recap</div>
      <div className="text-sm mb-6" style={{ color: COLORS.inkSoft }}>Speak naturally for 30–60 seconds about what was decided and who's doing what.</div>

      {micError && !useTyped && (
        <div className="p-3 rounded-lg text-sm mb-4" style={{ background: COLORS.overdueSoft, color: COLORS.overdue }}>{micError}</div>
      )}

      {!useTyped && speechSupported && (
        <div className="flex flex-col items-center py-10 rounded-2xl border mb-4" style={{ borderColor: COLORS.border, background: COLORS.paperRaised }}>
          <LangToggle value={speechLang} onChange={onChangeSpeechLang} disabled={listening} />
          <button onClick={listening ? onStop : onStart} className="w-20 h-20 rounded-full flex items-center justify-center mb-4" style={{ background: listening ? COLORS.overdueSoft : COLORS.plum, color: listening ? COLORS.overdue : "white" }}>
            {listening ? <Square size={26} /> : <Mic size={28} />}
          </button>
          <div className="text-sm font-medium" style={{ color: listening ? COLORS.overdue : COLORS.inkSoft }}>{listening ? "Recording — tap to stop" : "Tap to start speaking"}</div>
          {listening && liveTranscript && <div className="mt-5 px-4 text-sm text-center" style={{ maxWidth: 380, color: COLORS.ink }}>{liveTranscript}</div>}
          {!listening && liveTranscript && (
            <div className="w-full px-6 mt-5">
              <div className="text-xs font-semibold uppercase mb-1.5" style={{ color: COLORS.inkSoft }}>Edit if anything's off</div>
              <textarea value={liveTranscript} onChange={(e) => setLiveTranscript(e.target.value)} rows={4}
                className="w-full p-3 rounded-xl border text-sm" style={{ borderColor: COLORS.border, color: COLORS.ink }} />
            </div>
          )}
        </div>
      )}

      {(useTyped || !speechSupported) && (
        <textarea value={typedFallback} onChange={(e) => setTypedFallback(e.target.value)}
          placeholder='e.g. "I will handle LinkedIn outreach, due Friday. Send the ABC proposal by Tuesday."'
          rows={7} className="w-full p-4 rounded-xl border text-sm mb-4" style={{ borderColor: COLORS.border }} />
      )}

      <button onClick={() => setUseTyped((v) => !v)} className="text-sm font-medium flex items-center gap-1.5 mb-6" style={{ color: COLORS.plum }}>
        <Type size={14} /> {useTyped ? "Use voice instead" : "Type it instead"}
      </button>

      <button disabled={!text || !text.trim()} onClick={() => onSubmit(text.trim())}
        className="w-full py-3.5 rounded-xl font-medium text-white flex items-center justify-center gap-2"
        style={{ background: COLORS.plum, opacity: (!text || !text.trim()) ? 0.3 : 1 }}>
        Process recap <ArrowRight size={16} />
      </button>
    </div>
  );
}

function ProcessingScreen() {
  return (
    <div className="text-center py-20">
      <Loader size={28} className="spin" style={{ color: COLORS.plum, margin: "0 auto 16px" }} />
      <div className="display text-lg mb-1">Finding the commitments…</div>
      <div className="text-sm" style={{ color: COLORS.inkSoft }}>Separating decisions from small talk.</div>
    </div>
  );
}

function ReviewScreen({ data, onUpdate, onFinish }) {
  const count = data.commitments.length;
  const decidedCount = data.commitments.filter((c) => c._approved !== null).length;
  const allDecided = decidedCount === count;
  return (
    <div>
      <div className="display text-xl font-medium mb-1">{count === 0 ? "No clear commitments found" : `Found ${count} commitment${count === 1 ? "" : "s"}`}</div>
      <div className="text-sm mb-6" style={{ color: COLORS.inkSoft }}>Review each one below. Nothing becomes active until you approve it.</div>

      {data.decisions && data.decisions.length > 0 && (
        <div className="mb-6 p-4 rounded-xl" style={{ background: COLORS.plumSoft }}>
          <div className="text-xs font-semibold uppercase mb-2" style={{ color: COLORS.plum }}>Decisions noted</div>
          {data.decisions.map((d, i) => <div key={i} className="text-sm mb-1">• {d.description}</div>)}
        </div>
      )}
      {count === 0 && <EmptyState icon={<FileText size={22} />} title="Nothing actionable detected" body="Try again with more specifics — who's doing what, and by when." />}

      <div className="flex flex-col gap-3 mb-6">
        {data.commitments.map((c) => <ReviewCard key={c._localId} item={c} onUpdate={(patch) => onUpdate(c._localId, patch)} />)}
      </div>

      {data.openQuestions && data.openQuestions.length > 0 && (
        <div className="mb-6 p-4 rounded-xl border" style={{ borderColor: COLORS.today, background: COLORS.todaySoft }}>
          <div className="text-xs font-semibold uppercase mb-2 flex items-center gap-1.5" style={{ color: COLORS.today }}><AlertTriangle size={13} /> Open questions</div>
          {data.openQuestions.map((q, i) => <div key={i} className="text-sm mb-1">{q.question}</div>)}
        </div>
      )}

      <button disabled={count > 0 && !allDecided} onClick={onFinish} className="w-full py-3.5 rounded-xl font-medium text-white" style={{ background: COLORS.plum, opacity: (count > 0 && !allDecided) ? 0.3 : 1 }}>
        {count === 0 ? "Continue" : allDecided ? "Save approved commitments" : `Review remaining (${count - decidedCount} left)`}
      </button>
    </div>
  );
}

function ReviewCard({ item, onUpdate }) {
  const needsOwner = !item.owner;
  const needsDate = !item.dueDate;
  const decided = item._approved !== null;
  return (
    <div className="p-4 rounded-xl border" style={{ borderColor: COLORS.border, background: item._approved === false ? COLORS.panel : COLORS.paperRaised, opacity: item._approved === false ? 0.55 : 1 }}>
      <div className="flex items-start justify-between gap-3 mb-2">
        <textarea value={item.description} onChange={(e) => onUpdate({ description: e.target.value })} rows={2}
          className="font-medium flex-1" style={{ fontSize: 15, resize: "none", background: "transparent", border: "none" }} />
        <ConfidenceBadge level={item.confidence} />
      </div>
      <SourceQuote text={item.sourceQuote} />
      <div className="flex flex-wrap gap-3 mt-3">
        <div className="flex items-center gap-1.5">
          <UserCircle size={15} style={{ color: needsOwner ? COLORS.overdue : COLORS.inkSoft }} />
          <input value={item.owner || ""} onChange={(e) => onUpdate({ owner: e.target.value })} placeholder="Who owns this? (defaults to Me)"
            className="text-sm py-0.5 px-1" style={{ width: 168, background: "transparent", borderBottom: `1px solid ${needsOwner ? COLORS.overdue : COLORS.border}`, color: needsOwner ? COLORS.overdue : COLORS.ink }} />
          {needsOwner && (
            <button onClick={() => onUpdate({ owner: "Me" })} className="text-xs font-medium" style={{ color: COLORS.plum }}>Assign to me</button>
          )}
        </div>
        <div className="flex items-center gap-1.5">
          <Calendar size={15} style={{ color: needsDate && item.dueDateText ? COLORS.today : COLORS.inkSoft }} />
          <input type="date" value={item.dueDate || ""} onChange={(e) => onUpdate({ dueDate: e.target.value, dueDateText: "" })} className="text-sm" style={{ background: "transparent", border: "none", color: COLORS.ink }} />
        </div>
      </div>
      {needsDate && item.dueDateText && (
        <div className="text-xs mt-2" style={{ color: COLORS.today }}>Said "{item.dueDateText}" — couldn't resolve to a date, pick one above</div>
      )}
      <div className="flex gap-2 mt-4">
        <button onClick={() => onUpdate({ _approved: true })} className="flex-1 py-2 rounded-lg text-sm font-medium flex items-center justify-center gap-1.5" style={{ background: item._approved === true ? COLORS.done : COLORS.doneSoft, color: item._approved === true ? "white" : COLORS.done }}><Check size={14} /> Approve</button>
        <button onClick={() => onUpdate({ _approved: false })} className="flex-1 py-2 rounded-lg text-sm font-medium flex items-center justify-center gap-1.5" style={{ background: item._approved === false ? COLORS.overdue : COLORS.border, color: item._approved === false ? "white" : COLORS.inkSoft }}><X size={14} /> Reject</button>
      </div>
      {!decided && <div className="text-xs mt-2 text-center" style={{ color: COLORS.inkSoft }}>Edit anything above, then approve or reject</div>}
    </div>
  );
}

function CommitmentDetail({ commitment, meeting, onBack, onUpdate }) {
  const [rescheduling, setRescheduling] = useState(false);
  const [newDate, setNewDate] = useState(commitment.dueAt || "");
  const [reassigning, setReassigning] = useState(false);
  const [newOwner, setNewOwner] = useState(commitment.owner || "");
  const bucket = statusBucket(commitment);
  return (
    <div>
      <BackRow onBack={onBack} />
      <div className="mt-4 mb-2 flex items-center gap-2">
        <StatusPill status={commitment.status === "active" ? bucket : commitment.status} />
        <ConfidenceBadge level={commitment.confidence} />
      </div>
      <div className="display text-xl font-medium mb-3">{commitment.description}</div>
      <div className="flex flex-col gap-2 text-sm mb-4" style={{ color: COLORS.inkSoft }}>
        <div className="flex items-center gap-2"><UserCircle size={15} /> {commitment.owner || "No owner set"}</div>
        <div className="flex items-center gap-2"><Calendar size={15} /> {fmtDate(commitment.dueAt)}</div>
        {meeting && <div className="flex items-center gap-2"><FileText size={15} /> From recap on {new Date(meeting.createdAt).toLocaleDateString()}</div>}
      </div>
      <SourceQuote text={commitment.sourceQuote} />
      {meeting && <div className="mt-4 p-3 rounded-lg text-sm" style={{ background: COLORS.panel, color: COLORS.inkSoft }}><div className="text-xs font-semibold uppercase mb-1">Full recap</div>{meeting.transcript}</div>}

      {commitment.status === "active" && (
        <div className="mt-6 flex flex-col gap-2">
          <div className="grid grid-cols-2 gap-2">
            <ActionButton icon={<CheckCircle2 size={16} />} label="Complete" tone="done" onClick={() => onUpdate({ status: "completed" })} />
            <ActionButton icon={<PauseCircle size={16} />} label="Dismiss" tone="muted" onClick={() => onUpdate({ status: "dismissed" })} />
          </div>
          <div className="grid grid-cols-2 gap-2">
            <ActionButton icon={<RotateCcw size={16} />} label="Reschedule" tone="plum" onClick={() => setRescheduling((v) => !v)} />
            <ActionButton icon={<UserCircle size={16} />} label="Reassign" tone="plum" onClick={() => setReassigning((v) => !v)} />
          </div>
          {rescheduling && (
            <div className="flex items-center gap-2 p-3 rounded-lg border" style={{ borderColor: COLORS.border }}>
              <input type="date" value={newDate} onChange={(e) => setNewDate(e.target.value)} className="text-sm flex-1" style={{ background: "transparent", border: "none" }} />
              <button onClick={() => { onUpdate({ dueAt: newDate }); setRescheduling(false); }} className="text-sm font-medium px-3 py-1.5 rounded-full text-white" style={{ background: COLORS.plum }}>Save</button>
            </div>
          )}
          {reassigning && (
            <div className="flex items-center gap-2 p-3 rounded-lg border" style={{ borderColor: COLORS.border }}>
              <input value={newOwner} onChange={(e) => setNewOwner(e.target.value)} placeholder="New owner (defaults to Me)" className="text-sm flex-1" style={{ background: "transparent", border: "none" }} />
              <button onClick={() => setNewOwner("Me")} className="text-xs font-medium" style={{ color: COLORS.plum }}>Me</button>
              <button onClick={() => { onUpdate({ owner: newOwner.trim() ? newOwner : "Me" }); setReassigning(false); }} className="text-sm font-medium px-3 py-1.5 rounded-full text-white" style={{ background: COLORS.plum }}>Save</button>
            </div>
          )}
        </div>
      )}
      {commitment.status !== "active" && (
        <button onClick={() => onUpdate({ status: "active" })} className="mt-6 w-full py-2.5 rounded-lg text-sm font-medium border" style={{ borderColor: COLORS.border, color: COLORS.inkSoft }}>Reopen this commitment</button>
      )}
    </div>
  );
}

function ActionButton({ icon, label, tone, onClick }) {
  const tones = { done: { bg: COLORS.doneSoft, fg: COLORS.done }, plum: { bg: COLORS.plumSoft, fg: COLORS.plum }, muted: { bg: COLORS.border, fg: COLORS.inkSoft } };
  const t = tones[tone] || tones.muted;
  return <button onClick={onClick} className="py-2.5 rounded-lg text-sm font-medium flex items-center justify-center gap-1.5" style={{ background: t.bg, color: t.fg }}>{icon} {label}</button>;
}

function SettingsScreen({ settings, onSave, onBack, onLogout }) {
  const [local, setLocal] = useState(() => ({ ...settings, phoneNumbers: settings.phoneNumbers || [] }));
  const [saved, setSaved] = useState(false);

  function updatePhone(i, value) {
    setLocal((l) => ({ ...l, phoneNumbers: l.phoneNumbers.map((p, idx) => (idx === i ? value : p)) }));
  }
  function removePhone(i) {
    setLocal((l) => ({ ...l, phoneNumbers: l.phoneNumbers.filter((_, idx) => idx !== i) }));
  }
  function addPhone() {
    setLocal((l) => ({ ...l, phoneNumbers: [...l.phoneNumbers, ""] }));
  }
  function handleSave() {
    onSave({ ...local, phoneNumbers: local.phoneNumbers.map((p) => p.trim()).filter(Boolean) });
    setSaved(true);
    setTimeout(() => setSaved(false), 1800);
  }

  return (
    <div>
      <BackRow onBack={onBack} />
      <div className="display text-xl font-medium mt-4 mb-1">Reminder settings</div>
      <div className="text-sm mb-6" style={{ color: COLORS.inkSoft }}>In-app reminders (the Today screen) always work. WhatsApp is optional — this server sends it directly via your own WhatsApp Business account.</div>

      <div className="p-4 rounded-xl border mb-4" style={{ borderColor: COLORS.border, background: COLORS.paperRaised }}>
        <div className="flex items-center justify-between mb-1">
          <div className="font-medium text-sm">Send WhatsApp reminders</div>
          <button onClick={() => setLocal((l) => ({ ...l, whatsappEnabled: !l.whatsappEnabled }))} className="rounded-full" style={{ width: 44, height: 24, padding: 0, border: "none", background: local.whatsappEnabled ? COLORS.done : COLORS.border, position: "relative", flexShrink: 0 }}>
            <span style={{ position: "absolute", top: 2, left: 2, width: 20, height: 20, borderRadius: 999, background: "white", transition: "transform .15s", transform: local.whatsappEnabled ? "translateX(20px)" : "translateX(0)" }} />
          </button>
        </div>
        <div className="text-xs" style={{ color: COLORS.inkSoft }}>Checks in whenever the app is open and something's due or overdue.</div>
      </div>

      {local.whatsappEnabled && (
        <div className="flex flex-col gap-3 mb-6">
          <div className="text-xs" style={{ color: COLORS.inkSoft }}>Reminders always go to your own WhatsApp. Optionally, also notify others:</div>
          {local.phoneNumbers.map((phone, i) => {
            const incomplete = phone && !/^\+\d{8,15}$/.test(phone.replace(/\s/g, ""));
            return (
              <div key={i}>
                <div className="flex items-center gap-2">
                  <input value={phone} onChange={(e) => updatePhone(i, e.target.value)} placeholder="+14155559876"
                    className="flex-1 p-3 rounded-lg border text-sm" style={{ borderColor: incomplete ? COLORS.today : COLORS.border }} />
                  <button onClick={() => removePhone(i)} className="p-2 rounded-lg" style={{ color: COLORS.inkSoft }} aria-label="Remove number"><X size={16} /></button>
                </div>
                {incomplete && <div className="text-xs mt-1" style={{ color: COLORS.today }}>Include the country code with a leading + (e.g. +919876543210), or reminders won't arrive.</div>}
              </div>
            );
          })}
          <button onClick={addPhone} className="text-sm font-medium self-start" style={{ color: COLORS.plum }}>+ Add another number</button>
          <div className="text-xs" style={{ color: COLORS.inkSoft }}>E.164 format: + followed by country code and number, no spaces.</div>
        </div>
      )}

      <button onClick={handleSave} className="w-full py-3 rounded-xl font-medium text-white" style={{ background: COLORS.plum }}>
        {saved ? "Saved" : "Save settings"}
      </button>

      <button onClick={onLogout} className="w-full py-3 rounded-xl font-medium mt-3 border" style={{ borderColor: COLORS.border, color: COLORS.inkSoft }}>
        Log out
      </button>
    </div>
  );
}

function MeetingHistory({ meetings, commitments, onOpen, onBack }) {
  return (
    <div>
      <BackRow onBack={onBack} />
      <div className="display text-xl font-medium mt-4 mb-6">Recap history</div>
      {meetings.length === 0 && <EmptyState icon={<FileText size={22} />} title="No recaps yet" body="Every recap you capture will show up here, along with the commitments it produced." />}
      <div className="flex flex-col gap-2">
        {meetings.map((m) => {
          const count = commitments.filter((c) => c.meetingId === m.id).length;
          return (
            <button key={m.id} onClick={() => onOpen(m.id)} className="text-left p-4 rounded-xl border flex items-center justify-between" style={{ borderColor: COLORS.border, background: COLORS.paperRaised }}>
              <div style={{ minWidth: 0 }}>
                <div className="text-sm font-medium mono">{new Date(m.createdAt).toLocaleString()}</div>
                <div className="text-sm mt-1" style={{ color: COLORS.inkSoft, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{m.transcript}</div>
                {m.status === "failed" ? (
                  <div className="text-xs mt-1 flex items-center gap-1" style={{ color: COLORS.overdue }}><AlertTriangle size={12} /> Extraction failed — try recapturing</div>
                ) : (
                  <div className="text-xs mt-1" style={{ color: COLORS.plum }}>{count} commitment{count === 1 ? "" : "s"} extracted</div>
                )}
              </div>
              <ChevronRight size={18} style={{ color: COLORS.inkSoft, flexShrink: 0 }} />
            </button>
          );
        })}
      </div>
    </div>
  );
}

function MeetingDetail({ meeting, commitments, onBack, onSelectCommitment }) {
  return (
    <div>
      <BackRow onBack={onBack} label="History" />
      <div className="text-xs mono mt-4 mb-1" style={{ color: COLORS.inkSoft }}>{new Date(meeting.createdAt).toLocaleString()}</div>
      <div className="display text-lg font-medium mb-4">Recap transcript</div>
      <div className="p-4 rounded-xl text-sm mb-6" style={{ background: COLORS.panel }}>{meeting.transcript}</div>
      {meeting.decisions && meeting.decisions.length > 0 && (
        <div className="mb-6">
          <div className="text-xs font-semibold uppercase mb-2" style={{ color: COLORS.inkSoft }}>Decisions</div>
          {meeting.decisions.map((d, i) => <div key={i} className="text-sm mb-1">• {d.description}</div>)}
        </div>
      )}
      <div className="text-xs font-semibold uppercase mb-2" style={{ color: COLORS.inkSoft }}>Commitments from this recap</div>
      <div className="flex flex-col gap-2">
        {commitments.length === 0 && <div className="text-sm" style={{ color: COLORS.inkSoft }}>None were approved from this recap.</div>}
        {commitments.map((c) => (
          <button key={c.id} onClick={() => onSelectCommitment(c.id)} className="text-left p-3 rounded-lg border flex items-center justify-between" style={{ borderColor: COLORS.border, background: COLORS.paperRaised }}>
            <div className="text-sm font-medium" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{c.description}</div>
            <ChevronRight size={16} style={{ color: COLORS.inkSoft }} />
          </button>
        ))}
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Root />);
