import React, { useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import {
  ArrowDownToLine,
  ArrowUpRight,
  Check,
  ChevronDown,
  CircleHelp,
  Copy,
  File,
  FolderLock,
  Globe2,
  Heart,
  Leaf,
  Link2,
  LoaderCircle,
  LockKeyhole,
  MessageCircle,
  Mic,
  MicOff,
  Paperclip,
  Phone,
  PhoneOff,
  Plus,
  Send,
  Settings,
  ShieldCheck,
  Trash2,
  Users,
  Video,
  VideoOff,
  X,
} from "lucide-react";
import { api } from "./api.js";
import {
  b64,
  random,
  identity,
  circleKeys,
  sealJSON,
  openJSON,
} from "./crypto.js";
import { upload, download } from "./files.js";
import { loadVault, saveVault, exportVault, importVault } from "./vault.js";
import { Calls } from "./calls.js";
import { metadataSchema, parseContent, signalSchema } from "./schemas.js";
import "./style.css";

function captureInvite() {
  if (location.hash.startsWith("#invite=")) {
    const encoded = location.hash.slice(8);
    history.replaceState(null, "", location.pathname + location.search);
    try {
      const value = JSON.parse(decodeURIComponent(encoded));
      if (
        !/^[\da-f-]{36}$/.test(value.room) ||
        !/^[\da-f-]{36}$/.test(value.id) ||
        !/^[\w-]{43}$/.test(value.key) ||
        !/^[\w-]{43}$/.test(value.token)
      )
        throw new Error();
      sessionStorage.setItem("candoramity.invite", JSON.stringify(value));
    } catch {
      sessionStorage.setItem(
        "candoramity.invite-error",
        "This invitation is malformed. Ask your friend for a fresh link.",
      );
    }
  }
  try {
    return JSON.parse(sessionStorage.getItem("candoramity.invite"));
  } catch {
    return null;
  }
}
const initialInvite = captureInvite();
const size = (n) =>
  n < 1024
    ? `${n} B`
    : n < 1048576
      ? `${(n / 1024).toFixed(1)} KB`
      : `${(n / 1048576).toFixed(1)} MB`;
const time = (date) =>
  new Date(date).toLocaleString([], {
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  });
const initials = (name) =>
  name
    ?.trim()
    .split(/\s+/)
    .map((n) => n[0])
    .slice(0, 2)
    .join("")
    .toUpperCase() || "?";
function Avatar({ name, small }) {
  return (
    <span className={`avatar ${small ? "small" : ""}`}>{initials(name)}</span>
  );
}
function Brand() {
  return (
    <div className="brand">
      <span className="brand-mark">
        <Leaf size={21} strokeWidth={1.7} />
      </span>
      candoramity<span className="brand-dot">.</span>
    </div>
  );
}
function Modal({ title, close, children, error }) {
  const ref = useRef();
  useEffect(() => {
    const el = ref.current;
    el.showModal();
    return () => el.close();
  }, []);
  return (
    <dialog
      ref={ref}
      aria-label={title}
      onCancel={close}
      onClick={(e) => {
        if (e.target === ref.current) close();
      }}
    >
      <div className="modal-head">
        <h2>{title}</h2>
        <button
          className="icon-button"
          onClick={close}
          aria-label="Close dialog"
        >
          <X size={20} />
        </button>
      </div>
      {error && (
        <div className="error" role="alert">
          {error}
        </div>
      )}
      {children}
    </dialog>
  );
}
function Media({ stream, muted, label }) {
  const ref = useRef();
  useEffect(() => {
    if (ref.current) ref.current.srcObject = stream;
  }, [stream]);
  return (
    <video aria-label={label} ref={ref} autoPlay playsInline muted={muted} />
  );
}

function App() {
  const [vault, setVault] = useState(loadVault),
    [me, setMe] = useState(null),
    [rooms, setRooms] = useState([]),
    [selected, setSelected] = useState(""),
    [view, setView] = useState("feed");
  const [events, setEvents] = useState([]),
    [members, setMembers] = useState([]),
    [config, setConfig] = useState(null),
    [callConfig, setCallConfig] = useState(null),
    [online, setOnline] = useState(false),
    [loading, setLoading] = useState(true);
  const [modal, setModal] = useState(null),
    [busy, setBusy] = useState(false),
    [error, setError] = useState(""),
    [notice, setNotice] = useState(""),
    [pending, setPending] = useState(initialInvite);
  const [text, setText] = useState(""),
    [attachment, setAttachment] = useState(null),
    [progress, setProgress] = useState(null),
    [call, setCall] = useState(null),
    [incoming, setIncoming] = useState(null);
  const [inviteList, setInviteList] = useState([]),
    [inviteURL, setInviteURL] = useState(""),
    [peer, setPeer] = useState(""),
    [replyTo, setReplyTo] = useState(null);
  const current = useRef({});
  current.current = { vault, me, rooms, selected, config };
  const socket = useRef(),
    calls = useRef(),
    signalHandler = useRef(),
    loadEventsRef = useRef(),
    sequence = useRef(0),
    chatEnd = useRef(),
    seenSignals = useRef(new Set());
  const room = rooms.find((r) => r.id === selected);
  const request = (path, options = {}) =>
    api(path, { token: current.current.vault?.token, ...options });
  const persist = (value) => {
    saveVault(value);
    setVault(value);
    current.current.vault = value;
  };
  const flash = (message) => {
    setNotice(message);
    setTimeout(() => setNotice(""), 5000);
  };
  const run = async (fn) => {
    setError("");
    setBusy(true);
    try {
      await fn();
    } catch (e) {
      setError(e.message || "Something went wrong. Please try again.");
    } finally {
      setBusy(false);
    }
  };

  async function refreshRooms(
    v = current.current.vault,
    author = current.current.me,
  ) {
    if (!v || !author) return;
    const records = await api("/rooms", { token: v.token }),
      result = [];
    for (const r of records) {
      if (!v.rooms[r.id]) continue;
      const keys = await circleKeys(v.rooms[r.id], r.id);
      const meta = metadataSchema.parse(
        await openJSON(r.meta, keys, `${r.id}/meta`, r.owner),
      );
      result.push({ ...r, ...meta, keys });
    }
    setRooms(result);
    current.current.rooms = result;
    setSelected((prev) =>
      result.some((r) => r.id === prev) ? prev : result[0]?.id || "",
    );
  }
  async function createCircle(
    name,
    v = current.current.vault,
    author = current.current.me,
  ) {
    const id = crypto.randomUUID(),
      secret = b64(random()),
      keys = await circleKeys(secret, id);
    const meta = await sealJSON(
      { name, description: "An invitation-only space for your people." },
      keys,
      author,
      `${id}/meta`,
    );
    // Save the key before sending the create request so a lost response cannot orphan it.
    const updated = { ...v, rooms: { ...v.rooms, [id]: secret } };
    persist(updated);
    await api("/rooms", { token: updated.token, body: { id, meta } });
    await refreshRooms(updated, author);
    setSelected(id);
    setView("feed");
    return id;
  }
  async function register(name) {
    const author = await identity();
    const device = await api("/devices", {
      body: { publicKey: b64(author.publicKey) },
    });
    const next = { ...device, seed: author.seed, name: name.trim(), rooms: {} };
    persist(next);
    setMe(author);
    current.current.me = author;
    if (!pending) await createCircle("My inner circle", next, author);
  }
  useEffect(() => {
    api("/health")
      .then(setConfig)
      .catch((e) => setError(e.message));
    const malformed = sessionStorage.getItem("candoramity.invite-error");
    if (malformed) {
      setError(malformed);
      sessionStorage.removeItem("candoramity.invite-error");
    }
    const stored = loadVault();
    if (!stored) {
      setLoading(false);
      return;
    }
    identity(stored.seed)
      .then(async (author) => {
        if (author.id !== stored.id)
          throw new Error("Device backup does not match its identity.");
        setMe(author);
        current.current.me = author;
        await refreshRooms(stored, author);
      })
      .catch((e) => setError(e.message))
      .finally(() => setLoading(false));
  }, []);
  async function redeem() {
    const next = {
      ...vault,
      rooms: { ...vault.rooms, [pending.room]: pending.key },
    };
    persist(next);
    try {
      await request("/invites/redeem", {
        body: { id: pending.id, token: pending.token },
      });
    } catch (e) {
      if (!e.message.includes("already belong")) throw e;
    }
    await refreshRooms(next, me);
    setSelected(pending.room);
    setPending(null);
    sessionStorage.removeItem("candoramity.invite");
    flash("You’re in. Welcome to the circle.");
  }
  async function refreshEvents() {
    const { selected: roomId, rooms: all } = current.current,
      r = all.find((r) => r.id === roomId);
    if (!r) return;
    const load = ++sequence.current;
    const records = [];
    let after = 0;
    while (true) {
      const batch = await request(`/rooms/${roomId}/events?after=${after}`);
      records.push(...batch);
      if (batch.length < 100) break;
      after = batch.at(-1).seq;
    }
    let invalid = 0;
    const decoded = [];
    for (const e of records) {
      try {
        decoded.push({
          ...e,
          content: parseContent(
            e.kind,
            await openJSON(
              e.envelope,
              r.keys,
              `${roomId}/${e.id}/${e.kind}`,
              e.author,
            ),
          ),
        });
      } catch {
        invalid++;
      }
    }
    if (load !== sequence.current || roomId !== current.current.selected)
      return;
    setEvents(decoded);
    if (invalid)
      setError(
        `${invalid} item(s) could not be authenticated and were hidden.`,
      );
    setMembers(await request(`/rooms/${roomId}/members`));
  }
  loadEventsRef.current = refreshEvents;
  useEffect(() => {
    setEvents([]);
    setMembers([]);
    setPeer("");
    setText("");
    setAttachment(null);
    setReplyTo(null);
    if (selected) run(refreshEvents);
  }, [selected]);
  useEffect(() => {
    if (view === "chat")
      chatEnd.current?.scrollIntoView({ behavior: "smooth", block: "end" });
  }, [events.length, view]);
  useEffect(() => {
    if (!vault?.token) return;
    api("/config", { token: vault.token })
      .then(setCallConfig)
      .catch((e) => setError(e.message));
  }, [vault?.token]);
  useEffect(() => {
    if (!vault?.token) return;
    let stopped = false,
      retry;
    async function connect() {
      try {
        const { ticket } = await api("/socket-ticket", {
          token: vault.token,
          body: {},
        });
        if (stopped) return;
        const ws = new WebSocket(
          `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/socket?ticket=${ticket}`,
        );
        socket.current = ws;
        ws.onopen = () => {
          setOnline(true);
          loadEventsRef.current?.().catch((e) => setError(e.message));
        };
        ws.onmessage = async ({ data }) => {
          try {
            const payload = JSON.parse(data);
            if (
              payload.type === "update" &&
              payload.room === current.current.selected
            )
              await loadEventsRef.current?.();
            if (payload.type === "signal")
              await signalHandler.current?.(payload);
          } catch (e) {
            setError(e.message);
          }
        };
        ws.onclose = () => {
          setOnline(false);
          if (!stopped) retry = setTimeout(connect, 2500);
        };
        ws.onerror = () => ws.close();
      } catch {
        if (!stopped) retry = setTimeout(connect, 5000);
      }
    }
    connect();
    return () => {
      stopped = true;
      clearTimeout(retry);
      socket.current?.close();
    };
  }, [vault?.token]);
  async function sendSignal(payload, to) {
    const { selected, rooms, me, vault } = current.current,
      r = rooms.find((r) => r.id === selected);
    if (!r || socket.current?.readyState !== WebSocket.OPEN)
      throw new Error("Reconnect before starting a call.");
    const envelope = await sealJSON(
      {
        ...payload,
        to,
        name: vault.name,
        id: crypto.randomUUID(),
        sent: Date.now(),
      },
      r.keys,
      me,
      `${selected}/signal/${me.id}`,
    );
    socket.current.send(JSON.stringify({ room: selected, to, envelope }));
  }
  signalHandler.current = async ({ room: roomId, from, envelope }) => {
    const r = current.current.rooms.find((r) => r.id === roomId);
    if (!r || !current.current.me) return;
    const data = signalSchema.parse(
      await openJSON(envelope, r.keys, `${roomId}/signal/${from}`, from),
    );
    if (
      data.to !== current.current.me.id ||
      Math.abs(Date.now() - data.sent) > 120000 ||
      seenSignals.current.has(data.id)
    )
      return;
    seenSignals.current.add(data.id);
    if (seenSignals.current.size > 1000)
      seenSignals.current.delete(seenSignals.current.values().next().value);
    if (roomId !== current.current.selected) {
      if (data.type === "offer")
        flash(`Missed call in ${r.name}. Open that circle to call back.`);
      return;
    }
    if (data.type === "end" || data.type === "reject")
      setIncoming((prev) =>
        prev?.callId === data.callId && prev.from === from ? null : prev,
      );
    await calls.current?.signal(data, from);
  };
  useEffect(() => {
    if (!me || !selected || !callConfig) return;
    const c = new Calls({
      me: me.id,
      send: sendSignal,
      config: callConfig,
      onState: (state, message) => {
        setCall(state);
        if (message) flash(message);
      },
      onIncoming: setIncoming,
    });
    calls.current = c;
    return () => {
      c.end(false);
      setIncoming(null);
    };
  }, [me?.id, selected, callConfig]);
  useEffect(() => {
    if (!incoming) return;
    const timeout = setTimeout(() => setIncoming(null), 45000);
    return () => clearTimeout(timeout);
  }, [incoming]);
  async function publish(kind, content, roomId = current.current.selected) {
    const r = current.current.rooms.find((r) => r.id === roomId);
    if (!r) throw new Error("Choose a circle first.");
    const id = crypto.randomUUID();
    const envelope = await sealJSON(
      parseContent(kind, { ...content, name: current.current.vault.name }),
      r.keys,
      current.current.me,
      `${r.id}/${id}/${kind}`,
    );
    await request(`/rooms/${r.id}/events`, { body: { id, kind, envelope } });
    await refreshEvents();
  }
  async function submit(e) {
    e.preventDefault();
    if (!text.trim() && !attachment) return;
    await run(async () => {
      let file;
      if (attachment) {
        setProgress(0);
        try {
          file = await upload(attachment, selected, vault.token, setProgress);
        } finally {
          setProgress(null);
        }
      }
      await publish(
        replyTo
          ? "comment"
          : view === "chat"
            ? "chat"
            : view === "files"
              ? "file"
              : "post",
        {
          text: text.trim(),
          ...(file ? { file } : {}),
          ...(replyTo ? { parent: replyTo } : {}),
        },
        selected,
      );
      if (current.current.selected === selected) {
        setText("");
        setAttachment(null);
        setReplyTo(null);
      }
    });
  }
  async function createInvite(e) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    await run(async () => {
      const invite = await request(`/rooms/${selected}/invites`, {
        body: { ttl: Number(data.get("ttl")), uses: Number(data.get("uses")) },
      });
      setInviteURL(
        `${location.origin}/#invite=${encodeURIComponent(JSON.stringify({ id: invite.id, token: invite.token, room: selected, key: vault.rooms[selected] }))}`,
      );
      setInviteList(await request(`/rooms/${selected}/invites`));
    });
  }
  async function openInvites() {
    setInviteURL("");
    setModal("invite");
    if (room.owner === me.id)
      await run(async () =>
        setInviteList(await request(`/rooms/${selected}/invites`)),
      );
  }
  const nameFor = (device) =>
    device === me?.id
      ? vault.name
      : [...events].reverse().find((e) => e.author === device)?.content.name ||
        `Friend ${device.slice(0, 6)}`;
  const peers = members.filter((m) => m.device !== me?.id);
  const posts = events.filter((e) => ["post", "file"].includes(e.kind));
  const files = events.filter((e) => e.content.file);
  const chats = events.filter((e) => e.kind === "chat");
  const selectedPeer = peer || peers[0]?.device || "";
  function switchRoom(id) {
    if (call) calls.current?.end();
    setSelected(id);
  }
  const downloadFile = (file) =>
    run(() => download(file, selected, vault.token));
  function fileCard(file) {
    return (
      <button
        className="file-card"
        onClick={() => downloadFile(file)}
        disabled={busy}
      >
        <span className="file-icon">
          <File size={22} />
        </span>
        <span>
          <strong>{file.name}</strong>
          <small>{size(file.size)} · Encrypted file</small>
        </span>
        <ArrowDownToLine size={18} />
      </button>
    );
  }

  if (loading)
    return (
      <div className="loading-screen">
        <Brand />
        <LoaderCircle className="spin" />
        <p>Opening your private space…</p>
      </div>
    );
  if (!vault)
    return (
      <div className="welcome">
        <div className="welcome-copy">
          <Brand />
          <div className="eyebrow">
            <span className="status-dot" /> A LITTLE CLOSER. A LOT MORE PRIVATE.
          </div>
          <h1>
            Your people.
            <br />
            Your space.
            <br />
            <em>Just yours.</em>
          </h1>
          <p>
            A quieter corner of the internet. Share the everyday, have a real
            conversation, and keep your inner circle close.
          </p>
          <div className="welcome-pills">
            <span>
              <LockKeyhole size={16} /> Encrypted by default
            </span>
            <span>
              <Users size={16} /> Invitation only
            </span>
          </div>
          <div className="welcome-art">
            <div className="orbit orbit-one" />
            <div className="orbit orbit-two" />
            <div className="art-center">
              <Leaf size={64} strokeWidth={1} />
            </div>
            <span className="art-note">
              Good company.
              <br />
              No audience required.
            </span>
          </div>
        </div>
        <div className="welcome-form">
          <span className="eyebrow">WELCOME TO CANDORAMITY</span>
          <h2>
            {pending ? "A friend saved you a seat." : "Make yourself at home."}
          </h2>
          <p>
            No email, no password, no public profile. Your identity starts on
            this device.
          </p>
          <form
            onSubmit={(e) => {
              e.preventDefault();
              run(() => register(new FormData(e.currentTarget).get("name")));
            }}
          >
            <label>
              Your display name
              <input
                name="name"
                placeholder="What should friends call you?"
                required
                maxLength={40}
                autoComplete="nickname"
              />
            </label>
            <button className="primary wide" disabled={busy}>
              {busy ? (
                <LoaderCircle className="spin" size={18} />
              ) : (
                <ArrowUpRight size={18} />
              )}
              Create my private space
            </button>
          </form>
          {error && (
            <div className="error" role="alert">
              {error}
            </div>
          )}
          <small className="quiet">
            Your keys stay in this browser. Save an encrypted backup after
            joining so you can return on another device.
          </small>
          <button className="text-button" onClick={() => setModal("restore")}>
            Already have a device backup? Restore it
          </button>
          <div className="welcome-security">
            <ShieldCheck size={22} />
            <div>
              <strong>Privacy has a place here.</strong>
              <small>Post-quantum encrypted content. No ads or tracking.</small>
            </div>
          </div>
        </div>
        {modal === "restore" && (
          <Restore
            close={() => setModal(null)}
            run={run}
            busy={busy}
            error={error}
          />
        )}
      </div>
    );

  return (
    <div className="app-shell">
      <aside className="sidebar">
        <Brand />
        <div className="workspace-label">
          YOUR PRIVATE SPACE <LockKeyhole size={12} />
        </div>
        <nav>
          {[
            ["feed", "Circle feed", Leaf],
            ["chat", "Messages", MessageCircle],
            ["files", "Shared files", FolderLock],
            ["calls", "Calls", Video],
          ].map(([key, label, Icon]) => (
            <button
              key={key}
              className={`nav-link ${view === key ? "active" : ""}`}
              onClick={() => {
                setView(key);
                setReplyTo(null);
              }}
            >
              <Icon size={19} />
              {label}
              {key === "chat" && chats.length > 0 && (
                <span className="count">{chats.length}</span>
              )}
            </button>
          ))}
        </nav>
        <div className="sidebar-heading">
          <span>MY CIRCLES</span>
          <button
            className="icon-button"
            aria-label="Create circle"
            onClick={() => setModal("circle")}
          >
            <Plus size={17} />
          </button>
        </div>
        <div className="circle-list">
          {rooms.map((r, i) => (
            <button
              disabled={busy}
              key={r.id}
              className={`circle-button ${selected === r.id ? "selected" : ""}`}
              onClick={() => switchRoom(r.id)}
            >
              <span className={`circle-symbol color-${i % 3}`}>
                {initials(r.name)}
              </span>
              <span>{r.name}</span>
              {selected === r.id && <span className="tiny-dot" />}
            </button>
          ))}
        </div>
        <button className="new-circle" onClick={() => setModal("circle")}>
          <Plus size={16} />
          Start a new circle
        </button>
        <div className="sidebar-bottom">
          <div className="private-note">
            <ShieldCheck size={20} />
            <strong>A space that stays yours.</strong>
            <p>
              Shared with your people.
              <br />
              Encrypted before it leaves.
            </p>
            <button className="text-button" onClick={() => setModal("privacy")}>
              How privacy works <ArrowUpRight size={13} />
            </button>
          </div>
          <button className="profile" onClick={() => setModal("settings")}>
            <Avatar name={vault.name} />
            <span>
              <strong>{vault.name}</strong>
              <small>Personal device</small>
            </span>
            <Settings size={18} />
          </button>
        </div>
      </aside>
      <div className="main-shell">
        <header className="topbar">
          <button
            className="breadcrumb"
            aria-label="Switch circle"
            onClick={() => setModal("switch")}
          >
            Your circles <span>/</span>
            <strong>{room?.name || "Welcome"}</strong>
            <ChevronDown size={14} />
          </button>
          <div className="topbar-right">
            <span className={`connection ${online ? "" : "offline"}`}>
              <span className="status-dot" />
              {online ? "Connected" : "Reconnecting"}
            </span>
            <button
              className="icon-button"
              aria-label="Privacy information"
              onClick={() => setModal("privacy")}
            >
              <CircleHelp size={19} />
            </button>
            <Avatar name={vault.name} small />
          </div>
        </header>
        <main className="workspace">
          {error && (
            <div className="error dismissible" role="alert">
              <span>{error}</span>
              <button
                className="icon-button"
                aria-label="Dismiss error"
                onClick={() => setError("")}
              >
                <X size={16} />
              </button>
            </div>
          )}
          {pending && (
            <div className="invite-banner">
              <div>
                <strong>You’ve been invited into a private circle.</strong>
                <p>Joining gives you access to this circle’s shared history.</p>
              </div>
              <button
                disabled={busy || !me}
                className="primary"
                onClick={() => run(redeem)}
              >
                Accept invitation <ArrowUpRight size={16} />
              </button>
              <button
                className="icon-button"
                aria-label="Dismiss invitation"
                onClick={() => {
                  setPending(null);
                  sessionStorage.removeItem("candoramity.invite");
                }}
              >
                <X size={18} />
              </button>
            </div>
          )}
          <div className="page-heading">
            <div>
              <span className="eyebrow">LESS NOISE. MORE CONNECTION.</span>
              <h1>
                {view === "feed"
                  ? "A little closer together."
                  : view === "chat"
                    ? "The conversation lives here."
                    : view === "files"
                      ? "Good things, shared."
                      : "Better face to face."}
              </h1>
              <p>
                {view === "feed"
                  ? "The moments that matter, with the people who matter."
                  : view === "chat"
                    ? "A private conversation with your circle. Pick up where you left off."
                    : view === "files"
                      ? "Your circle’s files, encrypted before they leave your device."
                      : "Make room for a real conversation. Audio and video, end to end encrypted."}
              </p>
            </div>
            {room && (
              <button className="primary" onClick={openInvites}>
                <Link2 size={17} />
                Invite a friend
              </button>
            )}
          </div>
          {!room ? (
            <div className="empty large">
              <Users size={38} />
              <h2>Your people belong here.</h2>
              <p>Accept your invitation or create a circle to get started.</p>
              <button className="primary" onClick={() => setModal("circle")}>
                <Plus size={16} />
                Create a circle
              </button>
            </div>
          ) : (
            <div className="content-grid">
              <section className="main-column">
                {view === "feed" && (
                  <div className="circle-hero">
                    <div>
                      <div className="hero-label">
                        <LockKeyhole size={13} /> YOUR INVITATION-ONLY CIRCLE
                      </div>
                      <h2>{room.name}</h2>
                      <p>A small space for your everyday.</p>
                      <div className="hero-bottom">
                        <span className="member-dots">
                          {members.slice(0, 3).map((m) => (
                            <Avatar
                              key={m.device}
                              name={nameFor(m.device)}
                              small
                            />
                          ))}
                        </span>
                        <span>
                          {members.length}{" "}
                          {members.length === 1 ? "member" : "members"} · Only
                          us here
                        </span>
                      </div>
                    </div>
                    <div className="hero-art" aria-hidden="true">
                      <span className="hero-ring" />
                      <Leaf size={66} strokeWidth={1.2} />
                      <span className="mini-star">✳</span>
                    </div>
                  </div>
                )}
                {["feed", "chat", "files"].includes(view) && (
                  <>
                    {view === "chat" && (
                      <div className="chat-panel">
                        <div className="section-title">
                          <h2>
                            <MessageCircle size={19} />
                            {room.name}
                          </h2>
                          <span className="secure-label">
                            <LockKeyhole size={12} />
                            Encrypted
                          </span>
                        </div>
                        <div className="chat-history">
                          {chats.length === 0 ? (
                            <div className="empty">
                              <MessageCircle size={34} />
                              <h3>Every good thing starts with hello.</h3>
                              <p>Invite a friend and start the conversation.</p>
                            </div>
                          ) : (
                            chats.map((e) => (
                              <div
                                key={e.id}
                                className={`chat-message ${e.author === me?.id ? "own" : ""}`}
                              >
                                <Avatar name={e.content.name} small />
                                <div>
                                  <div className="message-meta">
                                    <strong>{e.content.name}</strong>
                                    <time>{time(e.created)}</time>
                                  </div>
                                  <div className="bubble">
                                    <p>{e.content.text}</p>
                                    {e.content.file && fileCard(e.content.file)}
                                  </div>
                                </div>
                              </div>
                            ))
                          )}
                          <div ref={chatEnd} />
                        </div>
                      </div>
                    )}
                    <form className="composer" onSubmit={submit}>
                      <div className="composer-top">
                        <Avatar name={vault.name} />
                        <textarea
                          aria-label={view === "chat" ? "Message" : "Post text"}
                          placeholder={
                            replyTo
                              ? "Write a thoughtful reply…"
                              : view === "chat"
                                ? "Say something to your circle…"
                                : view === "files"
                                  ? "A note to go with your file…"
                                  : "What’s on your mind? You’re among friends."
                          }
                          value={text}
                          onChange={(e) => setText(e.target.value)}
                          maxLength={12000}
                          rows={view === "chat" ? 2 : 3}
                        />
                      </div>
                      {replyTo && (
                        <div className="attachment">
                          Replying to a post
                          <button
                            type="button"
                            className="icon-button"
                            aria-label="Cancel reply"
                            onClick={() => setReplyTo(null)}
                          >
                            <X size={14} />
                          </button>
                        </div>
                      )}
                      {attachment && (
                        <div className="attachment">
                          <Paperclip size={15} />
                          {attachment.name} · {size(attachment.size)}
                          <button
                            type="button"
                            className="icon-button"
                            aria-label="Remove attachment"
                            onClick={() => setAttachment(null)}
                          >
                            <X size={14} />
                          </button>
                        </div>
                      )}
                      <div className="composer-bottom">
                        <label className="attach-button">
                          <Paperclip size={17} />
                          <span>Attach a file</span>
                          <input
                            type="file"
                            aria-label="Attach a file"
                            onChange={(e) => {
                              setAttachment(e.target.files[0] || null);
                              e.target.value = "";
                            }}
                            disabled={busy}
                          />
                        </label>
                        <span className="composer-lock">
                          <LockKeyhole size={12} />
                          Only your circle
                        </span>
                        <button
                          className="primary small-button"
                          disabled={
                            busy ||
                            (!text.trim() && !attachment) ||
                            (view === "files" && !attachment)
                          }
                        >
                          {busy ? (
                            <LoaderCircle size={15} className="spin" />
                          ) : (
                            <Send size={15} />
                          )}{" "}
                          {progress !== null
                            ? `${progress}%`
                            : view === "chat"
                              ? "Send"
                              : "Share"}
                        </button>
                      </div>
                    </form>
                  </>
                )}
                {view === "feed" && (
                  <>
                    <div className="feed-label">
                      <h2>
                        In your circle <span>{posts.length}</span>
                      </h2>
                      <span>
                        Latest first <ChevronDown size={13} />
                      </span>
                    </div>
                    {posts.length ? (
                      [...posts].reverse().map((e) => {
                        const liked = events
                          .filter(
                            (r) =>
                              r.kind === "reaction" &&
                              r.content.parent === e.id,
                          )
                          .reduce(
                            (map, r) => map.set(r.author, r.content.active),
                            new Map(),
                          );
                        const replies = events.filter(
                          (r) =>
                            r.kind === "comment" && r.content.parent === e.id,
                        );
                        return (
                          <article className="post" key={e.id}>
                            <div className="post-header">
                              <Avatar name={e.content.name} />
                              <div>
                                <strong>{e.content.name}</strong>
                                <small>
                                  {time(e.created)} <span>·</span>{" "}
                                  <LockKeyhole size={11} />
                                </small>
                              </div>
                              {e.author === me?.id && (
                                <button
                                  className="icon-button delete-post"
                                  aria-label="Delete post"
                                  onClick={() =>
                                    run(async () => {
                                      await request(
                                        `/rooms/${selected}/events/${e.id}`,
                                        { method: "DELETE" },
                                      );
                                      await refreshEvents();
                                    })
                                  }
                                >
                                  <Trash2 size={16} />
                                </button>
                              )}
                            </div>
                            <p className="post-text">{e.content.text}</p>
                            {e.content.file && fileCard(e.content.file)}
                            <div className="post-actions">
                              <button
                                className={liked.get(me?.id) ? "liked" : ""}
                                disabled={busy}
                                onClick={() =>
                                  run(() =>
                                    publish("reaction", {
                                      parent: e.id,
                                      active: !liked.get(me.id),
                                    }),
                                  )
                                }
                              >
                                <Heart size={17} />
                                {[...liked.values()].filter(Boolean).length ||
                                  ""}{" "}
                                Appreciate
                              </button>
                              <button
                                onClick={() => {
                                  setReplyTo(e.id);
                                  document.querySelector("textarea")?.focus();
                                }}
                              >
                                <MessageCircle size={17} />
                                {replies.length || ""} Reply
                              </button>
                              <span>
                                <ShieldCheck size={13} />
                                Verified
                              </span>
                            </div>
                            {replies.map((reply) => (
                              <div className="reply" key={reply.id}>
                                <Avatar name={reply.content.name} small />
                                <div>
                                  <strong>{reply.content.name}</strong>
                                  <p>{reply.content.text}</p>
                                  {reply.content.file &&
                                    fileCard(reply.content.file)}
                                </div>
                              </div>
                            ))}
                          </article>
                        );
                      })
                    ) : (
                      <div className="empty feed-empty">
                        <div className="empty-illustration">
                          <Leaf size={37} strokeWidth={1.3} />
                          <span>✦</span>
                        </div>
                        <h2>A fresh start, together.</h2>
                        <p>
                          Share a thought, a photo, or something worth keeping.
                          <br />
                          Your circle’s story starts with you.
                        </p>
                        <button
                          className="text-button"
                          onClick={() =>
                            document.querySelector("textarea")?.focus()
                          }
                        >
                          Share your first moment <ArrowUpRight size={15} />
                        </button>
                      </div>
                    )}
                  </>
                )}
                {view === "files" && (
                  <div className="files-panel">
                    <div className="section-title">
                      <h2>Shared files</h2>
                      <span>{files.length} files</span>
                    </div>
                    {files.length ? (
                      [...files].reverse().map((e) => (
                        <div className="file-row" key={e.id}>
                          {fileCard(e.content.file)}
                          <small>
                            Shared by {e.content.name} · {time(e.created)}
                          </small>
                        </div>
                      ))
                    ) : (
                      <div className="empty">
                        <FolderLock size={36} />
                        <h3>A safe place for what you share.</h3>
                        <p>Add a file above. Up to 25 MiB per file.</p>
                      </div>
                    )}
                  </div>
                )}
                {view === "calls" && (
                  <div className="calls-panel">
                    <div className="call-illustration">
                      <Video size={42} strokeWidth={1.3} />
                    </div>
                    <h2>A familiar face. A proper catch-up.</h2>
                    <p>Connect directly with a friend in your circle.</p>
                    {peers.length > 0 ? (
                      <>
                        <label className="peer-select">
                          Call a friend
                          <select
                            aria-label="Friend to call"
                            value={selectedPeer}
                            onChange={(e) => setPeer(e.target.value)}
                          >
                            {peers.map((p) => (
                              <option key={p.device} value={p.device}>
                                {nameFor(p.device)}
                              </option>
                            ))}
                          </select>
                        </label>
                        <div className="button-row">
                          <button
                            className="secondary"
                            disabled={busy || !online || !!call || !callConfig}
                            onClick={() =>
                              run(() =>
                                calls.current.start(selectedPeer, false),
                              )
                            }
                          >
                            <Phone size={17} />
                            Audio call
                          </button>
                          <button
                            className="primary"
                            disabled={busy || !online || !!call || !callConfig}
                            onClick={() =>
                              run(() => calls.current.start(selectedPeer, true))
                            }
                          >
                            <Video size={17} />
                            Video call
                          </button>
                        </div>
                      </>
                    ) : (
                      <button className="primary" onClick={openInvites}>
                        <Link2 size={17} />
                        Invite someone to talk
                      </button>
                    )}
                    <div className="call-footnote">
                      <ShieldCheck size={15} />
                      Audio and video encrypted on your device.
                    </div>
                  </div>
                )}
              </section>
              <aside className="right-column">
                <div className="info-card circle-info">
                  <div className="section-title">
                    <h3>Our little corner</h3>
                    <Users size={17} />
                  </div>
                  <p>
                    Good conversations don’t need a crowd. Just the right
                    people.
                  </p>
                  <div className="members-list">
                    {members.slice(0, 6).map((m) => (
                      <button
                        className="member"
                        key={m.device}
                        aria-label={`View identity for ${nameFor(m.device)}`}
                        onClick={() => setModal(`member:${m.device}`)}
                      >
                        <Avatar name={nameFor(m.device)} small />
                        <span>
                          {nameFor(m.device)}
                          {m.device === me?.id && <small>That’s you</small>}
                        </span>
                        {m.device === room.owner && (
                          <span className="owner-tag">Owner</span>
                        )}
                      </button>
                    ))}
                  </div>
                  <button className="secondary wide" onClick={openInvites}>
                    <Plus size={16} />
                    Bring a friend
                  </button>
                </div>
                <div className="info-card privacy-card">
                  <span className="shield-tile">
                    <ShieldCheck size={25} />
                  </span>
                  <h3>
                    Connection without
                    <br />
                    the compromise.
                  </h3>
                  <p>
                    Your posts, messages, and files are encrypted before they
                    leave your device.
                  </p>
                  <div className="privacy-line">
                    <Check size={14} />
                    Post-quantum content encryption
                  </div>
                  <div className="privacy-line">
                    <Check size={14} />
                    No public profiles or ads
                  </div>
                  <div className="privacy-line">
                    <Check size={14} />
                    Disposable invitation links
                  </div>
                  <button
                    className="text-button"
                    onClick={() => setModal("privacy")}
                  >
                    A closer look at privacy <ArrowUpRight size={13} />
                  </button>
                </div>
                <div className="storage-note">
                  <Globe2 size={16} />
                  <div>
                    <strong>
                      {config?.storage === "ipfs"
                        ? config.available
                          ? "IPFS storage connected"
                          : "IPFS node unavailable"
                        : "Local storage mode"}
                    </strong>
                    <p>
                      {config?.storage === "ipfs"
                        ? "Only encrypted blocks reach IPFS."
                        : "Encrypted files stay on this server. Connect Kubo to use IPFS."}
                    </p>
                  </div>
                </div>
                <div className="sidebar-footer">
                  Made for people, not algorithms.
                  <br />
                  <span>Candoramity · A quieter kind of social.</span>
                </div>
              </aside>
            </div>
          )}
        </main>
      </div>
      {notice && (
        <div className="toast" role="status">
          <Check size={17} />
          {notice}
        </div>
      )}
      {call && (
        <div className="active-call">
          <div className="call-title">
            <ShieldCheck size={16} />
            <strong>{nameFor(call.peer)}</strong>
            <span>{call.status}</span>
          </div>
          <div className="videos">
            <Media stream={call.remote} label="Friend’s video" />
            <Media stream={call.local} muted label="Your video" />
          </div>
          <div className="call-controls">
            <button
              className="secondary"
              onClick={() => calls.current.mute()}
              aria-label={call.muted ? "Unmute microphone" : "Mute microphone"}
            >
              {call.muted ? <MicOff size={18} /> : <Mic size={18} />}
            </button>
            <button
              className="secondary"
              onClick={() => calls.current.camera()}
              aria-label="Toggle camera"
            >
              {call.cameraOff ? <VideoOff size={18} /> : <Video size={18} />}
            </button>
            <button className="danger" onClick={() => calls.current.end()}>
              <PhoneOff size={18} />
              End call
            </button>
          </div>
        </div>
      )}
      {incoming && (
        <Modal
          error={error}
          title={`${incoming.name || "A friend"} is calling`}
          close={() =>
            run(async () => {
              await sendSignal(
                { type: "reject", callId: incoming.callId },
                incoming.from,
              );
              setIncoming(null);
            })
          }
        >
          <p>
            An encrypted {incoming.video ? "video" : "audio"} call in{" "}
            {room?.name}.
          </p>
          <div className="button-row">
            <button
              className="secondary"
              onClick={() =>
                run(async () => {
                  await sendSignal(
                    { type: "reject", callId: incoming.callId },
                    incoming.from,
                  );
                  setIncoming(null);
                })
              }
            >
              Decline
            </button>
            <button
              className="primary"
              disabled={busy}
              onClick={() =>
                run(async () => {
                  const offer = incoming;
                  setIncoming(null);
                  await calls.current.accept(offer, offer.from);
                })
              }
            >
              <Phone size={17} />
              Accept call
            </button>
          </div>
        </Modal>
      )}
      {modal === "switch" && (
        <Modal error={error} title="Your circles" close={() => setModal(null)}>
          <div className="switch-circles">
            {rooms.map((r) => (
              <button
                className="secondary wide"
                key={r.id}
                onClick={() => {
                  switchRoom(r.id);
                  setModal(null);
                }}
              >
                <Users size={16} />
                {r.name}
                {r.id === selected && <Check size={16} />}
              </button>
            ))}
          </div>
          <button className="primary wide" onClick={() => setModal("circle")}>
            <Plus size={16} />
            Create a new circle
          </button>
        </Modal>
      )}
      {modal === "circle" && (
        <Modal
          error={error}
          title="Start a new circle"
          close={() => setModal(null)}
        >
          <p>A private space for a few good people.</p>
          <form
            onSubmit={(e) => {
              e.preventDefault();
              const name = new FormData(e.currentTarget).get("name").trim();
              if (name)
                run(async () => {
                  await createCircle(name);
                  setModal(null);
                });
            }}
          >
            <label>
              Circle name
              <input
                name="name"
                required
                maxLength={60}
                placeholder="The weekend people"
                autoFocus
              />
            </label>
            <button className="primary wide" disabled={busy}>
              <Plus size={17} />
              Create circle
            </button>
          </form>
        </Modal>
      )}
      {modal === "invite" && (
        <Modal
          error={error}
          title="Good company starts here."
          close={() => setModal(null)}
        >
          <p>
            Share an invitation to <strong>{room.name}</strong>. Anyone who
            redeems it can read this circle’s full history and future updates.
          </p>
          {room.owner !== me?.id ? (
            <div className="soft-box">
              Ask the circle owner for an invitation link.
            </div>
          ) : (
            <>
              <form onSubmit={createInvite}>
                <div className="form-row">
                  <label>
                    Expires in
                    <select name="ttl" defaultValue="86400">
                      <option value="3600">1 hour</option>
                      <option value="86400">24 hours</option>
                      <option value="604800">7 days</option>
                    </select>
                  </label>
                  <label>
                    Can be used
                    <select name="uses" defaultValue="1">
                      <option value="1">Once</option>
                      <option value="5">5 times</option>
                      <option value="20">20 times</option>
                    </select>
                  </label>
                </div>
                <button className="primary wide" disabled={busy}>
                  <Link2 size={16} />
                  Create invitation link
                </button>
              </form>
              {inviteURL && (
                <div className="created-link">
                  <label>
                    Your private invitation
                    <input
                      aria-label="Invitation URL"
                      value={inviteURL}
                      readOnly
                      onFocus={(e) => e.target.select()}
                    />
                  </label>
                  <button
                    className="secondary wide"
                    onClick={() =>
                      run(async () => {
                        await navigator.clipboard.writeText(inviteURL);
                        flash("Invitation copied. Share it privately.");
                      })
                    }
                  >
                    <Copy size={16} />
                    Copy link
                  </button>
                  <small>
                    Treat this link like a key. Share it through a trusted
                    channel.
                  </small>
                </div>
              )}
              <div className="invites-list">
                <h3>Your invitations</h3>
                {inviteList.length ? (
                  inviteList.map((i) => (
                    <div className="invite-row" key={i.id}>
                      <div>
                        <strong>
                          {i.revoked
                            ? "Revoked"
                            : i.remaining === 0
                              ? "Used"
                              : i.expires < Date.now()
                                ? "Expired"
                                : `${i.remaining} uses left`}
                        </strong>
                        <small>Expires {time(i.expires)}</small>
                      </div>
                      {!i.revoked &&
                        i.remaining > 0 &&
                        i.expires > Date.now() && (
                          <button
                            className="text-button"
                            disabled={busy}
                            onClick={() =>
                              run(async () => {
                                await request(
                                  `/rooms/${selected}/invites/${i.id}`,
                                  { method: "DELETE" },
                                );
                                setInviteList(
                                  await request(`/rooms/${selected}/invites`),
                                );
                                setInviteURL("");
                              })
                            }
                          >
                            Revoke
                          </button>
                        )}
                    </div>
                  ))
                ) : (
                  <p>No invitation links yet.</p>
                )}
              </div>
              <small className="quiet">
                Expiry and revocation stop future joins. They do not remove
                existing members or erase downloaded copies.
              </small>
            </>
          )}
        </Modal>
      )}
      {modal === "privacy" && (
        <Modal
          error={error}
          title="Privacy, in plain language."
          close={() => setModal(null)}
        >
          <div className="privacy-details">
            <ShieldCheck size={35} />
            <h3>Your content is encrypted on your device.</h3>
            <p>
              Messages, posts, and file keys use ML-KEM-768, AES-256-GCM, and
              ML-DSA-65 signatures. Calls encrypt audio and video frames with
              AES-256 keys delivered inside encrypted signaling.
            </p>
            <h3>Your circle shares access.</h3>
            <p>
              A joining member can read the circle’s history. Invitation expiry
              controls joining, not continued access. To change who can read
              future content, create a new circle with a new key.
            </p>
            <h3>Private content, visible connections.</h3>
            <p>
              The server sees device IDs, membership, timing, and encrypted
              content sizes. IPFS is public: encrypted blocks may persist or be
              copied. Direct calls can reveal your IP address to the other
              participant.
            </p>
            <h3>Your device holds the keys.</h3>
            <p>
              Keys are saved in this browser’s local storage. Protect this
              device and keep an encrypted backup. Compare your device
              fingerprint with friends through another trusted channel.
            </p>
            <h3>A new app, with honest limits.</h3>
            <p>
              This application protocol has not had an independent security
              audit. It does not provide forward secrecy, anonymity, or quantum
              key distribution. Post-quantum primitives are not a guarantee that
              the whole app is secure.
            </p>
          </div>
        </Modal>
      )}
      {modal?.startsWith("member:") && (
        <Modal title={nameFor(modal.slice(7))} close={() => setModal(null)}>
          <p>
            Compare this device fingerprint with your friend through a separate
            trusted channel. Display names can be changed or copied.
          </p>
          <div className="soft-box">
            <strong>Device fingerprint</strong>
            <code>{modal.slice(7)}</code>
          </div>
        </Modal>
      )}
      {modal === "settings" && (
        <Modal
          error={error}
          title="Your device & identity"
          close={() => setModal(null)}
        >
          <form
            onSubmit={(e) => {
              e.preventDefault();
              const name = new FormData(e.currentTarget).get("name").trim();
              if (name) {
                persist({ ...vault, name });
                flash("Display name updated for future posts.");
              }
            }}
          >
            <label>
              Display name
              <input
                name="name"
                defaultValue={vault.name}
                maxLength={40}
                required
              />
            </label>
            <button className="secondary" type="submit">
              Save name
            </button>
          </form>
          <div className="soft-box">
            <strong>Your device fingerprint</strong>
            <code>{vault.id}</code>
            <small>Compare this with friends to verify your identity.</small>
          </div>
          <h3>Keep a way back in.</h3>
          <p>
            An encrypted backup saves your identity and circle keys. Create a
            new backup after joining or creating circles.
          </p>
          <form
            onSubmit={(e) => {
              e.preventDefault();
              const password = new FormData(e.currentTarget).get("password");
              run(() => exportVault(vault, password));
            }}
          >
            <label>
              Backup password
              <input
                name="password"
                type="password"
                minLength={12}
                required
                autoComplete="new-password"
                placeholder="At least 12 characters"
              />
            </label>
            <button className="primary wide" disabled={busy}>
              <ArrowDownToLine size={16} />
              Download encrypted backup
            </button>
          </form>
          <button className="text-button" onClick={() => setModal("restore")}>
            Restore a device backup
          </button>
          <small className="quiet">
            Clearing browser data without a backup permanently loses your keys.
            The server cannot recover them.
          </small>
        </Modal>
      )}
      {modal === "restore" && (
        <Restore
          close={() => setModal(null)}
          run={run}
          busy={busy}
          error={error}
        />
      )}
    </div>
  );
}
function Restore({ close, run, busy, error }) {
  return (
    <Modal error={error} title="Welcome back." close={close}>
      <p>
        Restore your encrypted device backup. This replaces the identity
        currently stored in this browser.
      </p>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const data = new FormData(e.currentTarget);
          run(async () => {
            const vault = await importVault(
              data.get("backup"),
              data.get("password"),
            );
            const author = await identity(vault.seed);
            if (author.id !== vault.id)
              throw new Error("Invalid device identity.");
            await api("/rooms", { token: vault.token });
            saveVault(vault);
            location.reload();
          });
        }}
      >
        <label>
          Encrypted backup
          <input
            name="backup"
            type="file"
            accept="application/json,.json"
            required
          />
        </label>
        <label>
          Backup password
          <input
            name="password"
            type="password"
            required
            minLength={12}
            autoComplete="current-password"
          />
        </label>
        <button className="primary wide" disabled={busy}>
          Restore my space <ArrowUpRight size={17} />
        </button>
      </form>
    </Modal>
  );
}
createRoot(document.getElementById("root")).render(<App />);
