// Marco-Polliio — root app, server-backed identity + items + votes + comments.
//
// Wire-up: every visitor picks a name + face (and optionally an admin password)
// on the first screen; backend issues a Marco token stored in localStorage.
// The token is the bearer credential for every subsequent request.

const STORAGE_TOKEN = 'mp_token_v1';
const STORAGE_TAB   = 'mp_tab_v1';
const API_BASE      = '/api/v1/marco';
const POLL_MS       = 30000;

// ── API helper ─────────────────────────────────────────────────────────────
function getToken() { try { return localStorage.getItem(STORAGE_TOKEN) || ''; } catch { return ''; } }
function setToken(t) { try { t ? localStorage.setItem(STORAGE_TOKEN, t) : localStorage.removeItem(STORAGE_TOKEN); } catch {} }

class MarcoApiError extends Error {
  constructor(status, body) { super(body?.message || `HTTP ${status}`); this.status = status; this.body = body; }
}

async function apiFetch(path, opts = {}) {
  const headers = { 'content-type': 'application/json', ...(opts.headers || {}) };
  const tok = getToken();
  if (tok) headers.Authorization = `Marco ${tok}`;
  const res = await fetch(API_BASE + path, { ...opts, headers });
  let body = null;
  const text = await res.text();
  if (text) { try { body = JSON.parse(text); } catch { body = text; } }
  if (!res.ok) throw new MarcoApiError(res.status, body);
  return body;
}

const marcoApi = {
  createIdentity: (data)       => apiFetch('/identity',             { method: 'POST', body: JSON.stringify(data) }),
  me:             ()           => apiFetch('/me'),
  listItems:      (kind)       => apiFetch(`/items?kind=${kind}`),
  createItem:     (kind, data) => apiFetch('/items',                { method: 'POST', body: JSON.stringify({ kind, ...data }) }),
  updateItem:     (id, data)   => apiFetch(`/items/${id}`,          { method: 'PATCH', body: JSON.stringify(data) }),
  deleteItem:     (id)         => apiFetch(`/items/${id}`,          { method: 'DELETE' }),
  vote:           (id, kind)   => apiFetch(`/items/${id}/vote`,     { method: 'PUT', body: JSON.stringify({ kind }) }),
  toggleReaction: (id, emoji)  => apiFetch(`/items/${id}/reactions`,{ method: 'PUT', body: JSON.stringify({ emoji }) }),
  addComment:     (id, data)   => apiFetch(`/items/${id}/comments`, { method: 'POST', body: JSON.stringify(data) }),
  editComment:    (id, data)   => apiFetch(`/comments/${id}`,       { method: 'PATCH', body: JSON.stringify(data) }),
  deleteComment:  (id)         => apiFetch(`/comments/${id}`,       { method: 'DELETE' }),
};

// ── Shape adapter ──────────────────────────────────────────────────────────
// Convert backend MarcoItemView → the shape existing UI components expect.
function denormalizeItem(item) {
  return {
    id: item.id,
    kind: item.kind, // 'FEATURE' | 'SUGGESTION'
    ts: new Date(item.createdAt).getTime(),
    author: item.author.name,
    authorId: item.author.id,
    title: item.title,
    description: item.description,
    media: item.media,
    voteCounts: item.voteCounts, // { up, down, neutral }
    myVote: item.myVote ? item.myVote.toLowerCase() : null, // 'up'|'down'|'neutral'|null
    reactions: item.reactions || {}, // { '🔥': { count, mine }, ... }
    comments: item.comments.map((c) => ({
      id: c.id,
      userId: c.author.id,
      name: c.author.name,
      role: (c.author.role || 'USER').toLowerCase(),
      avatar: c.author.avatar,
      ts: new Date(c.createdAt).getTime(),
      text: c.text,
      attachments: c.attachments,
      audio: !!c.audio,
      edited: !!c.editedAt,
      editedTs: c.editedAt ? new Date(c.editedAt).getTime() : undefined,
    })),
  };
}

// Optimistic-vote helper — mutate counts + myVote without touching the
// rest of the item. Server reply overwrites this within ~80ms.
function applyOptimisticVote(item, newKind) {
  const counts = { ...item.voteCounts };
  const prev = item.myVote;
  if (prev && counts[prev] != null) counts[prev] = Math.max(0, counts[prev] - 1);
  if (newKind && counts[newKind] != null) counts[newKind] = counts[newKind] + 1;
  return { ...item, voteCounts: counts, myVote: newKind };
}

// Optimistic reaction toggle — flip mine + adjust count for that emoji.
function applyOptimisticReaction(item, emoji) {
  const reactions = { ...(item.reactions || {}) };
  const cell = reactions[emoji] || { count: 0, mine: false };
  if (cell.mine) {
    const next = Math.max(0, cell.count - 1);
    if (next === 0) delete reactions[emoji];
    else reactions[emoji] = { count: next, mine: false };
  } else {
    reactions[emoji] = { count: cell.count + 1, mine: true };
  }
  return { ...item, reactions };
}

// ── App ────────────────────────────────────────────────────────────────────
function App() {
  const [user, setUser]               = React.useState(null);
  const [bootstrapped, setBoot]       = React.useState(false);
  const [bootError, setBootError]     = React.useState('');
  const [tab, setTab]                 = React.useState(() => {
    try { return localStorage.getItem(STORAGE_TAB) || 'poll'; } catch { return 'poll'; }
  });
  const [features, setFeatures]       = React.useState([]);
  const [suggestions, setSuggestions] = React.useState([]);
  const [loadingItems, setLoadingItems] = React.useState(false);
  const [listError, setListError]     = React.useState('');
  const [showAdd, setShowAdd]         = React.useState(false);
  const [editTarget, setEditTarget]   = React.useState(null);
  const [drawCallback, setDrawCallback] = React.useState(null);
  const [lightbox, setLightbox]       = React.useState(null);
  const [annotateTarget, setAnnotateTarget] = React.useState(null);

  React.useEffect(() => { try { localStorage.setItem(STORAGE_TAB, tab); } catch {} }, [tab]);
  React.useEffect(() => { document.body.setAttribute('data-tab', tab); }, [tab]);

  // Boot: validate stored token; fall through to identity picker if none/401.
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        if (!getToken()) { if (!cancelled) setBoot(true); return; }
        const r = await marcoApi.me();
        if (!cancelled) { setUser(r.user); setBoot(true); }
      } catch (err) {
        if (cancelled) return;
        if (err.status === 401) setToken('');
        else setBootError(err.message || "Couldn't reach the backend.");
        setBoot(true);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const fetchItems = React.useCallback(async (showSpinner = true) => {
    if (!user) return;
    if (showSpinner) setLoadingItems(true);
    setListError('');
    try {
      const kind = tab === 'poll' ? 'feature' : 'suggestion';
      const rows = await marcoApi.listItems(kind);
      const itemsList = rows.map(denormalizeItem);
      if (tab === 'poll') setFeatures(itemsList); else setSuggestions(itemsList);
    } catch (err) {
      if (err.status === 401) { setToken(''); setUser(null); return; }
      setListError(err.message || 'Failed to load.');
    } finally {
      if (showSpinner) setLoadingItems(false);
    }
  }, [user, tab]);

  React.useEffect(() => { fetchItems(true); }, [fetchItems]);

  // Background polling so other users' changes show up without a refresh.
  React.useEffect(() => {
    if (!user) return;
    const t = setInterval(() => { fetchItems(false); }, POLL_MS);
    return () => clearInterval(t);
  }, [user, fetchItems]);

  const submitIdentity = async ({ name, avatar, password }) => {
    try {
      const r = await marcoApi.createIdentity({ name, avatar, password: password || undefined });
      setToken(r.token);
      setUser(r.user);
      return null;
    } catch (err) {
      return err.body?.message || err.message || "Couldn't create identity.";
    }
  };

  const logout = () => { setToken(''); setUser(null); setFeatures([]); setSuggestions([]); };

  // active collection + helpers
  const items     = tab === 'poll' ? features : suggestions;
  const setItems  = tab === 'poll' ? setFeatures : setSuggestions;
  const noun      = tab === 'poll' ? 'feature' : 'suggestion';
  const isAdmin   = user?.role === 'ADMIN';
  const canCreate = !!user && (tab === 'poll' ? isAdmin : true);
  const canManage = (item) => !!user && (isAdmin || item.authorId === user.id);
  const canCommentManage = (c) => !!user && (isAdmin || c.userId === user.id);

  // Every backend mutator returns the freshly-viewed parent — single-call replace.
  const replaceItem = (kind, view) => {
    const denorm = denormalizeItem(view);
    const updater = (xs) => {
      const idx = xs.findIndex((x) => x.id === denorm.id);
      if (idx === -1) return [denorm, ...xs];
      const copy = xs.slice();
      copy[idx] = denorm;
      return copy;
    };
    if (kind === 'FEATURE') setFeatures(updater); else setSuggestions(updater);
  };

  // Mutators
  const addItem = async ({ title, description, media }) => {
    try {
      const kind = tab === 'poll' ? 'feature' : 'suggestion';
      const view = await marcoApi.createItem(kind, { title, description, media });
      replaceItem(view.kind, view);
      setShowAdd(false);
    } catch (err) { alert(err.body?.message || err.message || "Couldn't create."); }
  };
  const updateItem = async ({ title, description, media }) => {
    const target = editTarget;
    try {
      const view = await marcoApi.updateItem(target.id, { title, description, media });
      replaceItem(view.kind, view);
      setEditTarget(null);
    } catch (err) { alert(err.body?.message || err.message || "Couldn't update."); }
  };
  const deleteItem = async (id) => {
    if (!confirm(`Delete this ${noun} and all its comments?`)) return;
    try {
      await marcoApi.deleteItem(id);
      setItems((xs) => xs.filter((x) => x.id !== id));
    } catch (err) { alert(err.body?.message || err.message || "Couldn't delete."); }
  };
  const vote = async (id, kind) => {
    setItems((xs) => xs.map((x) => x.id !== id ? x : applyOptimisticVote(x, kind)));
    try {
      const view = await marcoApi.vote(id, kind);
      replaceItem(view.kind, view);
    } catch (err) { console.warn('Vote failed', err); fetchItems(false); }
  };
  const toggleReaction = async (id, emoji) => {
    setItems((xs) => xs.map((x) => x.id !== id ? x : applyOptimisticReaction(x, emoji)));
    try {
      const view = await marcoApi.toggleReaction(id, emoji);
      replaceItem(view.kind, view);
    } catch (err) { console.warn('Reaction failed', err); fetchItems(false); }
  };
  const addComment = async (id, { text, attachments, audio }) => {
    try {
      const view = await marcoApi.addComment(id, { text, attachments, audio: audio ? 'voice' : null });
      replaceItem(view.kind, view);
    } catch (err) { alert(err.body?.message || err.message || "Couldn't post comment."); }
  };
  const editComment = async (_itemId, commentId, { text }) => {
    try {
      const view = await marcoApi.editComment(commentId, { text });
      replaceItem(view.kind, view);
    } catch (err) { alert(err.body?.message || err.message || "Couldn't edit comment."); }
  };
  const deleteComment = async (_itemId, commentId) => {
    if (!confirm('Delete this comment?')) return;
    try {
      const view = await marcoApi.deleteComment(commentId);
      replaceItem(view.kind, view);
    } catch (err) { alert(err.body?.message || err.message || "Couldn't delete comment."); }
  };

  const onAnnotateSave = (dataUrl) => {
    if (!annotateTarget) return;
    addComment(annotateTarget.featureId, { text: '✎ annotation', attachments: [dataUrl] });
    setAnnotateTarget(null);
  };

  const requestDraw = (cb) => setDrawCallback(() => cb);
  const onDrawSave = (src) => { drawCallback?.(src); setDrawCallback(null); };
  const openNew = (which) => {
    const targetTab = which === 'feature' ? 'poll' : 'suggestions';
    if (tab !== targetTab) setTab(targetTab);
    setShowAdd(true);
  };

  if (!bootstrapped) return <div className="boot-splash" />;
  if (!user) return <IdentityScreen onSubmit={submitIdentity} initialError={bootError} />;

  // UI components expect role lower-case ('admin' / 'user'); normalize once.
  const userView = { ...user, role: user.role.toLowerCase() };

  return (
    <>
      <TopNav user={userView} onLogout={logout} />
      <main className="canvas">
        <PollHero />
        <Tabs
          active={tab}
          onChange={setTab}
          canCreateFeature={isAdmin}
          onNewFeature={() => openNew('feature')}
          onNewSuggestion={() => openNew('suggestion')}
        />

        {listError && <div className="list-error">{listError}</div>}

        {loadingItems && items.length === 0 ? (
          <div className="empty-state"><h3>loading…</h3></div>
        ) : items.length === 0 ? (
          <div className="empty-state">
            <h3>nothing posted yet</h3>
            {tab === 'poll'
              ? (canCreate
                  ? <p>Hit <em>new feature</em> in the toolbar above to seed the first one.</p>
                  : <p>The dev hasn't posted anything yet. Check back soon.</p>)
              : <p>Be the first to suggest something — hit <em>new suggestion</em> in the toolbar above.</p>}
          </div>
        ) : (
          <div className="stack">
            {items.map((x) => (
              <FeatureRow
                key={x.id}
                feature={x}
                user={userView}
                canManage={canManage(x)}
                canCommentManage={canCommentManage}
                onVote={vote}
                onReact={toggleReaction}
                onComment={addComment}
                onDelete={deleteItem}
                onEdit={(t) => setEditTarget(t)}
                onEditComment={editComment}
                onDeleteComment={deleteComment}
                onZoom={(src, featureId) => setLightbox({ src, featureId: featureId || x.id })}
                onRequestDraw={requestDraw}
              />
            ))}
          </div>
        )}
      </main>

      {showAdd && (
        <FeatureModal noun={noun} onClose={() => setShowAdd(false)} onSave={addItem} />
      )}
      {editTarget && (
        <FeatureModal noun={noun} initial={editTarget} onClose={() => setEditTarget(null)} onSave={updateItem} />
      )}
      {drawCallback && (
        <DrawModal onClose={() => setDrawCallback(null)} onSave={onDrawSave} />
      )}
      {annotateTarget && (
        <AnnotateModal
          imageSrc={annotateTarget.src}
          onClose={() => setAnnotateTarget(null)}
          onSave={onAnnotateSave}
        />
      )}
      <Lightbox
        lightbox={lightbox}
        onClose={() => setLightbox(null)}
        onAnnotate={(src, featureId) => { setLightbox(null); setAnnotateTarget({ src, featureId }); }}
      />
    </>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
