/* global React, ReactDOM */
const { useState: useS, useEffect: useE, useMemo: useM, useCallback: useC } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "palette": "warm",
  "showMotif": true,
  "nameMode": "full",
  "showBanner": true
}/*EDITMODE-END*/;

const CURRENT_FOCUS = {
  title: 'What could the hall be used for, for the benefit of the town most, over the summer school break?',
  body: 'Six weeks of long evenings, restless children, and quieter weekdays. Tell us what you would set up if the door were open. Half-formed ideas welcome.'
};

/* ==========================================================
   Stage 2 — wired against the real back end (server/).

   The browser holds NO simulated state any more. The only
   pieces of persistence here are the tweaks panel (which
   round-trips through __edit_mode_set_keys to the source
   file) and a per-tab "ideas + members" cache so navigation
   doesn't refetch on every route change. The session lives
   in an httpOnly cookie set by the server.
   ========================================================== */

/**
 * Read the ?auth=… query param the server sets on redirect from
 * /auth/consume. Then strip it from the URL so it doesn't survive
 * a refresh. The value tells us what to flash in the toast / show
 * as a sign-in error.
 */
const readAuthFlag = () => {
  try {
    const url = new URL(window.location.href);
    const flag = url.searchParams.get('auth');
    if (flag) {
      url.searchParams.delete('auth');
      window.history.replaceState({}, '', url.toString());
    }
    return flag;
  } catch (e) { return null; }
};

const FLAG_MESSAGES = {
  ok:             { kind: 'toast', text: 'Signed in.' },
  invalid:        { kind: 'error', text: 'That sign-in link couldn\u2019t be opened. Request a new one below.' },
  unknown:        { kind: 'error', text: 'That sign-in link couldn\u2019t be opened. Request a new one below.' },
  used:           { kind: 'error', text: 'That link was already used. Request a new one below.' },
  expired:        { kind: 'error', text: 'That link has expired. Request a new one below.' },
  'not-invited':  { kind: 'error', text: 'This address isn\u2019t on the invite list yet.' },
  deactivated:    { kind: 'error', text: 'This account is no longer active. Get in touch with LEAF.' }
};

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Boot state. 'loading' while we resolve /me; 'ready' once we know.
  const [boot, setBoot] = useS({ status: 'loading', error: null });
  const [user, setUser] = useS(null);
  const [route, setRoute] = useS('welcome');
  const [ideas, setIdeas] = useS([]);          // list items + lazily-merged detail fields
  const [pendingClaims, setPendingClaims] = useS([]);
  const [toast, setToast] = useS(null);
  const [signInFlash, setSignInFlash] = useS(null);  // {kind:'error'|'toast', text}

  // Apply palette
  useE(() => {
    document.documentElement.setAttribute('data-palette', t.palette);
  }, [t.palette]);

  const showToast = useC((msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 2400);
  }, []);

  /* ===== Boot ===== */
  useE(() => {
    let cancelled = false;
    const flag = readAuthFlag();
    if (flag && FLAG_MESSAGES[flag]) setSignInFlash(FLAG_MESSAGES[flag]);

    (async () => {
      try {
        const me = await window.api.me();
        if (cancelled) return;
        if (me) {
          await loadAuthedData(me);
          setUser(me);
          if (flag === 'ok') showToast('Welcome back.');
        }
        setBoot({ status: 'ready', error: null });
      } catch (e) {
        if (cancelled) return;
        setBoot({ status: 'error', error: e?.message || 'Could not reach the server' });
      }
    })();

    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Fetches the data an authed user needs to navigate the room.
  // Always re-fetches members so we get any new invites.
  const loadAuthedData = useC(async (me) => {
    const [members, ideaList] = await Promise.all([
      window.api.listMembers(),
      window.api.listIdeas()
    ]);
    window.setMembers(members);
    setIdeas(ideaList);
    if (me?.isLeaf) {
      try {
        const claims = await window.api.pendingClaims();
        setPendingClaims(claims);
      } catch (e) { /* non-fatal — admin queue just stays empty */ }
    } else {
      setPendingClaims([]);
    }
  }, []);

  /* ===== Helpers to keep `ideas` in sync ===== */

  // Replace an idea in the list (merging detail fields if present).
  const mergeIdea = useC((next) => {
    setIdeas((prev) => {
      const idx = prev.findIndex((i) => i.id === next.id);
      if (idx === -1) return [next, ...prev];
      const merged = { ...prev[idx], ...next };
      const copy = prev.slice();
      copy[idx] = merged;
      return copy;
    });
  }, []);

  // Re-fetch the LEAF pending-claims queue. Called after any action
  // that adds, removes, approves, or declines a claim.
  const refreshPendingClaims = useC(async () => {
    if (!user?.isLeaf) return;
    try { setPendingClaims(await window.api.pendingClaims()); }
    catch (e) { /* non-fatal */ }
  }, [user]);

  // Lazy-load the FULL detail for an idea when navigating to its
  // page. We only do this if we haven't loaded it before — the
  // detail fields (comments, edits, claimRequests, myRanking) are
  // absent on the list shape.
  const ensureIdeaDetail = useC(async (id) => {
    const existing = ideas.find((i) => i.id === id);
    if (existing && Array.isArray(existing.comments)) return;
    try {
      const detail = await window.api.getIdea(id);
      mergeIdea(detail);
    } catch (e) {
      if (e instanceof window.ApiError && e.status === 404) {
        showToast('Idea not found.');
        setRoute('voice');
      }
    }
  }, [ideas, mergeIdea, showToast]);

  useE(() => {
    if (!user) return;
    if (route.startsWith('idea:')) ensureIdeaDetail(route.slice(5));
  }, [route, user, ensureIdeaDetail]);

  /* ===== Auth handlers ===== */

  // Issue a magic link. The server always returns 204 (it never
  // leaks whether the address is registered), so we just confirm
  // the request was accepted.
  const handleRequestLink = async (rawEmail) => {
    const email = (rawEmail || '').trim();
    if (!email) return false;
    try {
      await window.api.requestLink(email);
      return true;
    } catch (e) {
      // Network/server error — the UI shows a soft retry message.
      return false;
    }
  };

  const handleSignOut = async () => {
    try { await window.api.signOut(); }
    catch (e) { /* ignore — local state goes anyway */ }
    setUser(null);
    setIdeas([]);
    setPendingClaims([]);
    setRoute('welcome');
  };

  /* ===== Mutations =====
     Every mutation hits the API, gets back the updated idea, and
     merges it into local state. There are no optimistic updates;
     the call is fast and the server response is the truth. */

  const handleComment = async (ideaId, text) => {
    try {
      const updated = await window.api.postComment(ideaId, text);
      mergeIdea(updated);
      showToast('Comment added');
    } catch (e) { showToast('Couldn\u2019t post comment'); }
  };

  const handleRank = async (ideaId, key, value) => {
    try {
      const updated = await window.api.rankAxis(ideaId, key, value);
      mergeIdea(updated);
      showToast(key === 'need' ? 'Need recorded' : 'Practicality recorded');
    } catch (e) { showToast('Couldn\u2019t save ranking'); }
  };

  const handleSubmitIdea = async (data) => {
    try {
      const created = await window.api.createIdea(data);
      mergeIdea(created);
      setRoute('idea:' + created.id);
      showToast('Idea posted. You\u2019re the first owner.');
    } catch (e) { showToast('Couldn\u2019t post idea'); }
  };

  const handleChangeStage = async (ideaId, stage) => {
    try {
      const updated = await window.api.setStage(ideaId, stage);
      mergeIdea(updated);
      showToast('Moved to ' + stage);
    } catch (e) { showToast('Couldn\u2019t change stage'); }
  };

  const handleSetResponse = async (ideaId, text) => {
    try {
      const updated = await window.api.setLeafResponse(ideaId, text);
      mergeIdea(updated);
      showToast('LEAF response posted');
    } catch (e) { showToast('Couldn\u2019t save response'); }
  };

  const handleInvite = async (email, _note, opts = {}) => {
    try {
      await window.api.invite({
        email,
        first: opts.first || email.split('@')[0],
        last: opts.last || '',
        role: opts.role || '',
        isLeaf: !!opts.isLeaf
      });
      // Refresh the members map so the new face appears in the directory.
      try { window.setMembers(await window.api.listMembers()); } catch (e) { /* ignore */ }
      showToast('Invitation sent to ' + email);
      return { ok: true };
    } catch (e) {
      const msg = e instanceof window.ApiError && e.status === 409
        ? 'That address is already on the invite list'
        : 'Couldn\u2019t send invitation';
      showToast(msg);
      return { ok: false, error: msg };
    }
  };

  /* ===== Ownership / claims ===== */

  const handleRequestClaim = async (ideaId, message) => {
    try {
      const updated = await window.api.requestClaim(ideaId, message);
      mergeIdea(updated);
      refreshPendingClaims();
      showToast('Claim sent to LEAF');
    } catch (e) { showToast('Couldn\u2019t send claim'); }
  };

  const handleCancelClaim = async (ideaId) => {
    try {
      const updated = await window.api.withdrawClaim(ideaId);
      mergeIdea(updated);
      refreshPendingClaims();
      showToast('Claim withdrawn');
    } catch (e) { showToast('Couldn\u2019t withdraw claim'); }
  };

  const handleApproveClaim = async (ideaId, memberId) => {
    try {
      const updated = await window.api.approveClaim(ideaId, memberId);
      mergeIdea(updated);
      refreshPendingClaims();
      const m = window.OB_DATA.MEMBER_BY_ID[memberId];
      showToast(`${m?.first || 'Member'} is now an owner`);
    } catch (e) { showToast('Couldn\u2019t approve'); }
  };

  const handleDeclineClaim = async (ideaId, memberId) => {
    try {
      const updated = await window.api.declineClaim(ideaId, memberId);
      mergeIdea(updated);
      refreshPendingClaims();
      showToast('Claim declined');
    } catch (e) { showToast('Couldn\u2019t decline'); }
  };

  const handleStepBack = async (ideaId) => {
    try {
      const updated = await window.api.stepBack(ideaId);
      mergeIdea(updated);
      showToast('You have stepped back from this idea');
    } catch (e) { showToast('Couldn\u2019t step back'); }
  };

  const handleEditIdea = async (ideaId, draft) => {
    try {
      const updated = await window.api.editIdea(ideaId, draft);
      mergeIdea(updated);
      showToast('Refinements saved');
    } catch (e) {
      const msg = e instanceof window.ApiError && e.status === 403
        ? 'Only owners can edit this idea' : 'Couldn\u2019t save refinements';
      showToast(msg);
    }
  };

  const handleImportIdeas = async (parsed) => {
    try {
      await window.api.bulkImport(parsed);
      // Refresh the list to pick up the newly created ideas.
      const list = await window.api.listIdeas();
      setIdeas(list);
      showToast(`Imported ${parsed.length} ${parsed.length === 1 ? 'idea' : 'ideas'} \u2014 awaiting owners`);
    } catch (e) { showToast('Couldn\u2019t import'); }
  };

  /* ============== Routing ============== */

  // While we resolve the session, render a quiet splash so the page
  // doesn't flash through SignIn → Welcome.
  if (boot.status === 'loading') {
    return (
      <div className="signin-wrap">
        <div className="signin-card" style={{ textAlign: 'center', paddingTop: 60 }}>
          <HallMark size={64} color="var(--olive-deep)" />
          <p style={{ marginTop: 24, color: 'var(--ink-faint)', fontStyle: 'italic' }}>
            Opening the room\u2026
          </p>
        </div>
      </div>
    );
  }

  if (boot.status === 'error') {
    return (
      <div className="signin-wrap">
        <div className="signin-card">
          <h1 style={{ fontStyle: 'italic' }}>The room is unreachable just now.</h1>
          <p className="note">{boot.error}</p>
          <button className="btn btn-primary" onClick={() => window.location.reload()}>Try again</button>
        </div>
      </div>
    );
  }

  if (!user) {
    return (
      <SignIn
        onRequestLink={handleRequestLink}
        flash={signInFlash}
        onDismissFlash={() => setSignInFlash(null)}
      />
    );
  }

  const isLeaf = user.isLeaf;
  let body;
  if (route === 'welcome') {
    body = <Welcome onNav={setRoute} ideas={ideas} showBanner={t.showBanner} currentFocus={CURRENT_FOCUS} currentUser={user} nameMode={t.nameMode} />;
  } else if (route === 'voice') {
    body = <CommunityVoice ideas={ideas} onNav={setRoute} nameMode={t.nameMode} currentUserId={user.id} />;
  } else if (route === 'submit') {
    body = <Submit onSubmit={handleSubmitIdea} onNav={setRoute} />;
  } else if (route === 'library') {
    body = <Library onNav={setRoute} focused={null} />;
  } else if (route.startsWith('lib:')) {
    body = <Library onNav={setRoute} focused={route.slice(4)} />;
  } else if (route === 'members') {
    body = <Members onInvite={handleInvite} nameMode={t.nameMode} isLeaf={isLeaf} />;
  } else if (route === 'admin') {
    body = isLeaf
      ? <Admin
          ideas={ideas}
          pendingClaims={pendingClaims}
          onChangeStage={handleChangeStage}
          onSetResponse={handleSetResponse}
          onNav={setRoute}
          onApproveClaim={handleApproveClaim}
          onDeclineClaim={handleDeclineClaim}
          onImportIdeas={handleImportIdeas}
          showToast={showToast}
        />
      : <Welcome onNav={setRoute} ideas={ideas} showBanner={t.showBanner} currentFocus={CURRENT_FOCUS} currentUser={user} nameMode={t.nameMode} />;
  } else if (route.startsWith('idea:')) {
    const id = route.slice(5);
    const idea = ideas.find(i => i.id === id);
    if (idea && Array.isArray(idea.comments)) {
      body = (
        <IdeaDetail
          idea={idea}
          onNav={setRoute}
          onComment={handleComment}
          onRank={handleRank}
          onRequestClaim={handleRequestClaim}
          onCancelClaim={handleCancelClaim}
          onStepBack={handleStepBack}
          onEditIdea={handleEditIdea}
          onApproveClaim={handleApproveClaim}
          onDeclineClaim={handleDeclineClaim}
          currentUser={user}
          nameMode={t.nameMode}
        />
      );
    } else {
      // Detail still loading (or idea missing). Show a quiet placeholder.
      body = (
        <div className="page">
          <p style={{ fontStyle: 'italic', color: 'var(--ink-faint)' }}>Opening idea\u2026</p>
        </div>
      );
    }
  }

  const topbarKey = route.startsWith('idea:') ? 'idea' : route.startsWith('lib:') ? 'library' : route === 'submit' ? 'voice' : route;

  // Pending-claims badge in the topbar — comes from the LEAF-only
  // /admin/pending-claims endpoint, kept in sync with mutations.
  const pendingCount = isLeaf ? pendingClaims.length : 0;

  return (
    <div className="app">
      <Topbar
        current={topbarKey}
        onNav={setRoute}
        currentUser={user}
        onSignOut={handleSignOut}
        showMotif={t.showMotif}
        nameMode={t.nameMode}
        adminBadge={pendingCount}
      />
      <div className="content">{body}</div>
      <div className="footer-quiet">
        St Katherine&rsquo;s Hall &nbsp;&middot;&nbsp; Community Voice &nbsp;&middot;&nbsp; LEAF &middot; Charity 1194047
      </div>

      {toast && (
        <div style={{
          position: 'fixed', bottom: 28, left: '50%', transform: 'translateX(-50%)',
          background: 'var(--olive-deep)', color: '#f6f1e0',
          padding: '10px 20px', borderRadius: 999,
          fontSize: 13, fontFamily: 'var(--sans)',
          boxShadow: '0 12px 30px -10px rgba(0,0,0,0.3)',
          zIndex: 200, letterSpacing: '0.02em'
        }}>
          {toast}
        </div>
      )}

      <TweaksPanel title="Tweaks">
        <TweakSection label="Palette">
          <TweakRadio
            label="Colour world"
            value={t.palette}
            onChange={v => setTweak('palette', v)}
            options={[
              { value: 'warm', label: 'Warm' },
              { value: 'quiet', label: 'Quiet' },
              { value: 'heritage', label: 'Heritage' }
            ]}
          />
        </TweakSection>

        <TweakSection label="Identity">
          <TweakToggle
            label="Show hall motif"
            value={t.showMotif}
            onChange={v => setTweak('showMotif', v)}
          />
          <TweakRadio
            label="Names"
            value={t.nameMode}
            onChange={v => setTweak('nameMode', v)}
            options={[
              { value: 'full', label: 'Full' },
              { value: 'initials', label: 'F. Initial' }
            ]}
          />
        </TweakSection>

        <TweakSection label="Welcome page">
          <TweakToggle
            label="Current-focus banner"
            value={t.showBanner}
            onChange={v => setTweak('showBanner', v)}
          />
        </TweakSection>

        <TweakSection label="Try a flow">
          <TweakButton label="Community Voice" onClick={() => setRoute('voice')} />
          {ideas.length > 0 && (
            <TweakButton label={'Open: ' + ideas[0].title.slice(0, 32) + '\u2026'} onClick={() => setRoute('idea:' + ideas[0].id)} />
          )}
          <TweakButton label="Submit a new idea" onClick={() => setRoute('submit')} />
          {isLeaf && <TweakButton label="LEAF host view" onClick={() => setRoute('admin')} />}
          <TweakButton label="Sign out" onClick={handleSignOut} secondary />
        </TweakSection>
      </TweaksPanel>
    </div>
  );
}

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