// app.jsx — v2 multi-tenant orchestrator. account → groups → group view.
(function () {
  const { useState, useEffect, useRef } = React;
  window.PFCtx = React.createContext(null);

  // faint darkened from #a8956f for WCAG-readable small text on the cream bg
  const RETRO = { light: true, bg: '#f0e3c6', surface: '#f8efd6', surface2: '#e8d8b4', border: 'rgba(60,42,20,0.20)', glass: 'rgba(240,227,198,0.82)', text: '#2c2316', sub: '#6a5a3f', faint: '#82703f' };
  const DISPLAY = "'Zen Antique', 'Shippori Mincho', serif";
  // per-user selectable body fonts (all preloaded in index.html)
  const FONTS = {
    zen: { label: '標準ゴシック', css: "'Zen Kaku Gothic New', sans-serif" },
    noto: { label: 'Noto', css: "'Noto Sans JP', sans-serif" },
    maru: { label: '丸ゴシック', css: "'Zen Maru Gothic', sans-serif" },
    rounded: { label: 'まるっこ', css: "'M PLUS Rounded 1c', sans-serif" },
  };
  function makeTheme(accent, font) {
    return { ...RETRO, accent: accent || '#d8442f', retro: true, coverStyle: 'retro', dense: false, body: (FONTS[font] || FONTS.zen).css, display: DISPLAY };
  }
  window.PF_FONTS = FONTS;
  function hueFromId(s) { let h = 0; for (let i = 0; i < (s || '').length; i++) h = (h * 31 + s.charCodeAt(i)) % 360; return h; }
  function relTime(epoch) {
    const d = Math.max(0, Math.floor(Date.now() / 1000) - epoch);
    if (d < 60) return 'たった今'; if (d < 3600) return Math.floor(d / 60) + '分';
    if (d < 86400) return Math.floor(d / 3600) + '時間'; return Math.floor(d / 86400) + '日';
  }
  const normAuthor = (a) => ({ ...a, passed: !!a.passed, papa: a.role === 'admin', color: hueFromId(a.id) });
  function normPost(p) {
    return { ...p, time: relTime(p.created_at), author: normAuthor(p.author),
      replies: (p.replies || []).map((r) => ({ ...r, time: relTime(r.created_at), author: normAuthor(r.author) })) };
  }
  const normAlbum = (a) => ({ id: a.id, title: a.title, desc: a.description || '', service: a.service, playlistUrl: a.playlist_url || '', tracks: a.tracks || [] });

  function App() {
    const [boot, setBoot] = useState('loading');   // loading | auth | pick | group
    // per-user appearance prefs (device-local; not group data)
    const [prefs, setPrefs] = useState(() => { try { return JSON.parse(localStorage.getItem('pf_prefs') || '{}'); } catch { return {}; } });
    const setPref = (k, v) => setPrefs((p) => { const n = { ...p }; if (v == null) delete n[k]; else n[k] = v; try { localStorage.setItem('pf_prefs', JSON.stringify(n)); } catch {} return n; });
    const [hasGroups, setHasGroups] = useState(false);
    const [account, setAccount] = useState(null);
    const [groups, setGroups] = useState([]);       // my memberships' groups
    const [gid, setGid] = useState(null);
    const [group, setGroup] = useState(null);       // current group meta + my membership
    const [posts, setPosts] = useState([]);
    const [albums, setAlbums] = useState([]);
    const [notifs, setNotifs] = useState({ items: [], unread: 0 });
    const initialCode = useRef(new URLSearchParams(location.search).get('code') || '').current;
    const initialShare = useRef((() => { const p = new URLSearchParams(location.search); const t = (p.get('url') || p.get('text') || ''); const m = t.match(/https?:\/\/\S+/); return m ? m[0] : ''; })()).current;
    const shareConsumed = useRef(false);

    // transient UI
    const [tab, setTabState] = useState('home');
    const [detailId, setDetailId] = useState(null);
    const [composing, setComposing] = useState(false);
    const [examOpen, setExamOpen] = useState(false);
    const [albumDetail, setAlbumDetail] = useState(null);
    const [albumEditor, setAlbumEditor] = useState(false);
    const [editingAlbum, setEditingAlbum] = useState(null);
    const [adminOpen, setAdminOpen] = useState(false);
    const [notifOpen, setNotifOpen] = useState(false);
    const [picker, setPicker] = useState(false);    // group switcher / create
    const [toast, setToast] = useState(null);


    function showToast(m) { setToast(m); clearTimeout(showToast._t); showToast._t = setTimeout(() => setToast(null), 1900); }

    const api = window.PFApi;
    const gpath = (id, p) => `/groups/${id}${p}`;

    const loadGroup = async (id) => {
      const g = (await api.get(gpath(id, ''))).membership ? await api.get(gpath(id, '')) : null;
      // group meta is in /me groups list; find it
      const meta = groups.find((x) => x.id === id) || (await api.get('/groups')).groups.find((x) => x.id === id);
      const m = await api.get(gpath(id, ''));   // {membership}
      const feed = await api.get(gpath(id, '/feed'));
      const al = await api.get(gpath(id, '/albums'));
      setGid(id);
      setGroup({ ...meta, membership: m.membership });
      setPosts(feed.posts.map(normPost));
      setAlbums(al.albums.map(normAlbum));
      loadNotifs();
      setBoot('group'); setPicker(false); setTabState('home'); setDetailId(null);
    };

    const loadNotifs = async () => { try { const n = await api.get('/notifications'); setNotifs({ items: n.notifications, unread: n.unread }); } catch {} };

    const enter = async () => {
      const me = await api.get('/me');
      setAccount(me.account); setGroups(me.groups);
      if (me.groups.length === 0) { setBoot('pick'); return; }
      await loadGroupWith(me.groups, me.groups[0].id);
    };
    // variant that takes a fresh groups list (avoids stale state)
    const loadGroupWith = async (gs, id) => {
      const meta = gs.find((x) => x.id === id);
      const m = await api.get(gpath(id, ''));
      const feed = await api.get(gpath(id, '/feed'));
      const al = await api.get(gpath(id, '/albums'));
      setGid(id); setGroup({ ...meta, membership: m.membership });
      setPosts(feed.posts.map(normPost)); setAlbums(al.albums.map(normAlbum));
      loadNotifs(); setBoot('group'); setPicker(false); setTabState('home'); setDetailId(null);
    };

    useEffect(() => {
      (async () => {
        let bs = { has_groups: false, authed: false };
        try { bs = await api.get('/bootstrap'); } catch {}
        setHasGroups(bs.has_groups);
        if (!bs.authed) { setBoot('auth'); return; }
        try { await enter(); } catch { setBoot('auth'); }
      })();
    }, []);

    const guard = (fn) => async (...a) => { try { return await fn(...a); } catch (e) { if (e.status === 401) { setAccount(null); setBoot('auth'); } else { showToast('うまくいきませんでした。通信状況を確認してください'); } } };
    const reloadFeed = async () => { const f = await api.get(gpath(gid, '/feed')); setPosts(f.posts.map(normPost)); };
    const refreshMembership = async () => { const m = await api.get(gpath(gid, '')); setGroup((g) => ({ ...g, membership: m.membership })); };
    const reloadAlbums = async () => { const al = await api.get(gpath(gid, '/albums')); setAlbums(al.albums.map(normAlbum)); };

    // auto-open compose in link mode when launched via share / ?url=
    useEffect(() => { if (boot === 'group' && initialShare && !shareConsumed.current) { shareConsumed.current = true; setComposing(true); } }, [boot]);

    // OS/browser back closes the open overlay instead of exiting the app
    const anyOverlay = !!detailId || composing || examOpen || albumEditor || !!albumDetail || adminOpen || notifOpen || picker;
    useEffect(() => { if (anyOverlay && !(history.state && history.state.pfOverlay)) history.pushState({ pfOverlay: true }, ''); }, [anyOverlay]);
    useEffect(() => {
      const onPop = () => {
        if (detailId) return setDetailId(null);
        if (notifOpen) return setNotifOpen(false);
        if (picker) return setPicker(false);
        if (adminOpen) return setAdminOpen(false);
        if (albumEditor) { setAlbumEditor(false); return setEditingAlbum(null); }
        if (albumDetail) return setAlbumDetail(null);
        if (examOpen) return setExamOpen(false);
        if (composing) return setComposing(false);
      };
      window.addEventListener('popstate', onPop);
      return () => window.removeEventListener('popstate', onPop);
    }, [detailId, notifOpen, picker, adminOpen, albumEditor, albumDetail, examOpen, composing]);

    // theme + me view
    // personal accent/font override the group default (group color is the fallback)
    const theme = makeTheme(prefs.accent || (group && group.accent), prefs.font || (group && group.font));
    useEffect(() => { document.body.dataset.light = '1'; }, []);
    const meView = account ? { id: account.id, name: account.name, handle: account.handle, role: group?.membership?.role || 'member', passed: !!group?.membership?.passed, papa: group?.membership?.role === 'admin', color: hueFromId(account.id) } : null;

    const findItem = (id) => { for (const p of posts) { if (p.id === id) return p; for (const r of p.replies) if (r.id === id) return r; } return null; };

    const app = {
      theme, account, groups, gid, group,
      membership: group?.membership, user: meView, users: { you: meView },
      me: meView, trackById: window.PF.trackById, posts, albums, notifs,
      tab, detailId, editingAlbum,
      copy: { appName: group?.name || 'しんちゃんFM', persona: group?.persona_name || 'オーナー' },
      examTitle: group?.exam_title || '審査',
      sharedUrl: initialShare,
      prefs, setPref, groupAccent: group && group.accent,

      setTab(k) { setTabState(k); setDetailId(null); setComposing(false); setExamOpen(false); setAlbumDetail(null); setAlbumEditor(false); setAdminOpen(false); setNotifOpen(false); scrollTop(); },
      go(name, params) { if (name === 'detail') { setDetailId(params.id); scrollTop(); } },
      back() { setDetailId(null); },
      compose() { if (group?.membership?.passed) setComposing(true); else setExamOpen(true); },
      closeCompose() { setComposing(false); },
      startExam() { setExamOpen(true); }, closeExam() { setExamOpen(false); },
      openAdmin() { setAdminOpen(true); }, closeAdmin() { setAdminOpen(false); },
      openNotifs() { setNotifOpen(true); }, closeNotifs() { setNotifOpen(false); },
      openPicker() { setPicker(true); }, closePicker() { setPicker(false); },
      switchGroup(id) { loadGroup(id); },
      refreshMembership, reloadFeed,

      async logout() { try { await api.post('/logout', {}); } catch {} setAccount(null); setGroups([]); setGroup(null); setGid(null); setBoot('auth'); },

      // albums
      openAlbumDetail(id) { setAlbumDetail(id); }, closeAlbumDetail() { setAlbumDetail(null); },
      openAlbumEditor(al) { setEditingAlbum(al || null); setAlbumEditor(true); },
      closeAlbumEditor() { setAlbumEditor(false); setEditingAlbum(null); },
      saveAlbum: guard(async (al) => {
        const payload = { title: al.title, description: al.desc, service: al.service, playlist_url: al.playlistUrl, tracks: al.tracks };
        let saved;
        if (al.id && albums.some((a) => a.id === al.id)) saved = (await api.put(gpath(gid, '/albums/' + al.id), payload)).album;
        else saved = (await api.post(gpath(gid, '/albums'), payload)).album;
        await reloadAlbums(); setAlbumEditor(false); setEditingAlbum(null); setAlbumDetail(saved.id);
        showToast('アルバムを保存しました 🎵');
      }),

      // reactions (know/like/curious)
      reactions(obj) { return obj.reactions || {}; },
      myReactions(obj) { return obj.myReactions || []; },
      react: async (targetId, targetType, kind) => {
        // optimistic toggle (no full feed reload → instant + keeps scroll)
        const apply = (it) => {
          const has = (it.myReactions || []).includes(kind);
          const myR = has ? it.myReactions.filter((k) => k !== kind) : [...(it.myReactions || []), kind];
          const rx = { ...(it.reactions || {}) }; rx[kind] = (rx[kind] || 0) + (has ? -1 : 1); if (rx[kind] <= 0) delete rx[kind];
          return { ...it, myReactions: myR, reactions: rx };
        };
        setPosts((ps) => ps.map((p) => {
          if (targetType === 'post' && p.id === targetId) return apply(p);
          if (targetType === 'reply') return { ...p, replies: p.replies.map((r) => r.id === targetId ? apply(r) : r) };
          return p;
        }));
        try { await api.post(gpath(gid, '/reactions'), { target_id: targetId, target_type: targetType, kind }); }
        catch (e) { if (e.status === 401) { setAccount(null); setBoot('auth'); } else { showToast('うまくいきませんでした'); await reloadFeed(); } }
      },

      addReply: guard(async (postId, text) => { await api.post(gpath(gid, `/posts/${postId}/replies`), { text }); await reloadFeed(); showToast('リプを送信しました'); }),
      addPost: guard(async (trackId, text) => { await api.post(gpath(gid, '/posts'), { track_id: trackId, text }); await reloadFeed(); showToast('投稿しました 🎵'); }),
      addPostLink: guard(async (link, text) => { await api.post(gpath(gid, '/posts'), { link, text }); await reloadFeed(); showToast('投稿しました 🎵'); }),
      resolveLink: (url) => api.get('/resolve?url=' + encodeURIComponent(url)),
      searchSongs: (q) => api.get('/search?q=' + encodeURIComponent(q)),
      createInvite: async () => { const r = await api.post(gpath(gid, '/invites'), {}); await refreshMembership(); return r; },
      async markNotifsRead() { await api.post('/notifications/read', {}); setNotifs((n) => ({ ...n, unread: 0 })); },

      share() { showToast('リンクをコピーしました'); },
      toast: showToast,
    };

    function scrollTop() { const el = document.getElementById('pf-scroll'); if (el) el.scrollTop = 0; }

    if (boot === 'loading') return <div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: theme.bg, color: theme.faint }}>…</div>;
    if (boot === 'auth') return <window.AuthGate theme={theme} hasGroups={hasGroups} initialCode={initialCode} onAuthed={() => { history.replaceState({}, '', location.pathname); setBoot('loading'); enter().catch(() => setBoot('auth')); }} />;
    if (boot === 'pick') return <window.PFCtx.Provider value={app}><GroupPicker app={app} empty /></window.PFCtx.Provider>;

    let screen;
    if (detailId) screen = <window.DetailScreen app={app} postId={detailId} />;
    else if (tab === 'home') screen = <window.HomeScreen app={app} />;
    else if (tab === 'profile') screen = <window.ProfileScreen app={app} />;

    const overlay = composing || examOpen || albumEditor || albumDetail || adminOpen || notifOpen || picker;
    const showTab = !detailId && !overlay;

    return (
      <window.PFCtx.Provider value={app}>
        <div style={{ height: '100%', position: 'relative', overflow: 'hidden', background: theme.bg, color: theme.text, fontFamily: theme.body }}>
          <div id="pf-scroll" style={{ position: 'absolute', inset: 0, overflowY: 'auto', overflowX: 'hidden', paddingBottom: showTab ? 96 : 0 }}>{screen}</div>
          {showTab && <window.TabBar />}
          {detailId && <window.ReplyBar app={app} postId={detailId} />}
          {composing && <window.ComposeScreen app={app} />}
          {examOpen && <ExamOverlay app={app} />}
          {albumDetail && <window.AlbumDetail app={app} albumId={albumDetail} />}
          {albumEditor && <window.AlbumEditor app={app} />}
          {adminOpen && <window.AdminScreen app={app} />}
          {notifOpen && <NotifPanel app={app} />}
          {picker && <GroupPicker app={app} />}
          <div className="pf-grain" />
          {toast && <div style={{ position: 'absolute', left: '50%', bottom: showTab ? 110 : 60, transform: 'translateX(-50%)', background: theme.text, color: theme.bg, fontWeight: 700, fontSize: 13.5, padding: '10px 18px', borderRadius: 22, zIndex: 95, boxShadow: '0 8px 24px rgba(0,0,0,0.3)', whiteSpace: 'nowrap', maxWidth: '90%' }}>{toast}</div>}
        </div>
      </window.PFCtx.Provider>
    );
  }

  // ── Group picker / create ────────────────────────────────────
  function GroupPicker({ app, empty }) {
    const T = app.theme;
    const [creating, setCreating] = useState(empty);
    const [name, setName] = useState(''); const [persona, setPersona] = useState(''); const [code, setCode] = useState('');
    const [busy, setBusy] = useState(false); const [err, setErr] = useState('');
    const create = async () => {
      setErr(''); setBusy(true);
      try {
        const r = await window.PFApi.post('/groups', { name, persona_name: persona || 'オーナー', exam_title: `${persona || 'オーナー'}の審査`, create_code: code.trim() });
        const me = await window.PFApi.get('/me'); app.groups.length; // noop
        location.reload();
      } catch (e) { setErr(e.message === 'invalid_create_code' ? '作成コードが無効です' : 'エラー: ' + e.message); setBusy(false); }
    };
    return (
      <div style={{ position: 'absolute', inset: 0, zIndex: 90, background: T.bg, display: 'flex', flexDirection: 'column', padding: '64px 24px 40px', overflow: 'auto' }}>
        {!empty && <button onClick={() => app.closePicker()} style={{ position: 'absolute', top: 54, right: 18, background: 'none', border: 'none', color: T.text, cursor: 'pointer' }}><window.Icon.close size={24} /></button>}
        <h1 style={{ fontFamily: T.display, fontSize: 26, fontWeight: 900, color: T.text, margin: '0 0 4px' }}>グループ</h1>
        <p style={{ color: T.sub, fontSize: 13.5, marginTop: 0 }}>{empty ? 'まだ所属グループがありません。作るか、招待を受けてください。' : '切り替え、または新規作成。'}</p>
        {app.groups.map((g) => (
          <button key={g.id} onClick={() => app.switchGroup(g.id)} style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', textAlign: 'left', padding: '14px', marginTop: 10, borderRadius: 14, border: `1px solid ${g.id === app.gid ? T.accent : T.border}`, background: T.surface, cursor: 'pointer', font: 'inherit' }}>
            <div style={{ width: 38, height: 38, borderRadius: 10, background: g.accent || T.accent, color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 900 }}>{(g.name || '?').charAt(0)}</div>
            <div><div style={{ fontWeight: 800, color: T.text }}>{g.name}</div><div style={{ color: T.faint, fontSize: 12 }}>{g.role === 'admin' ? '管理者' : (g.passed ? '合格メンバー' : 'メンバー')}</div></div>
          </button>
        ))}
        <div style={{ borderTop: `1px solid ${T.border}`, margin: '20px 0 0', paddingTop: 18 }}>
          {!creating ? (
            <button onClick={() => setCreating(true)} style={btn(T)}>＋ 新しいグループを作る</button>
          ) : (
            <>
              <div style={{ color: T.faint, fontSize: 12.5, fontWeight: 700, marginBottom: 4 }}>新しいグループ</div>
              {app.groups.length >= 0 && <input value={code} onChange={(e) => setCode(e.target.value)} placeholder="作成コード（最初の1つは不要）" style={fld(T)} />}
              <input value={name} onChange={(e) => setName(e.target.value)} placeholder="グループ名" style={fld(T)} />
              <input value={persona} onChange={(e) => setPersona(e.target.value)} placeholder="審査の主（例: ◯◯のパパ）" style={fld(T)} />
              {err && <div style={{ color: '#c0392b', fontSize: 13, marginTop: 8 }}>{err}</div>}
              <button onClick={create} disabled={busy || !name.trim()} style={{ ...btn(T), opacity: busy || !name.trim() ? 0.5 : 1 }}>{busy ? '…' : '作成'}</button>
            </>
          )}
          <button onClick={() => app.logout()} style={{ ...btn(T), background: 'none', color: T.faint, border: `1px solid ${T.border}`, boxShadow: 'none' }}>ログアウト</button>
        </div>
      </div>
    );
  }

  // ── Notifications panel ──────────────────────────────────────
  function NotifPanel({ app }) {
    const T = app.theme;
    useEffect(() => { app.markNotifsRead(); }, []);
    const label = (k) => ({ reply: 'があなたの投稿にリプ', reaction: 'があなたの投稿にリアクション', invite_accepted: 'が招待から参加', exam_passed: '審査に合格しました', added_to_group: 'グループに追加', removed: 'グループから外れました' }[k] || k);
    return (
      <div style={{ position: 'absolute', inset: 0, background: T.bg, zIndex: 86, display: 'flex', flexDirection: 'column' }}>
        <div style={{ paddingTop: 54, padding: 'max(54px, env(safe-area-inset-top)) 14px 10px', borderBottom: `1px solid ${T.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <button onClick={() => app.closeNotifs()} style={{ background: 'none', border: 'none', color: T.text, cursor: 'pointer' }}><window.Icon.back size={22} /></button>
          <span style={{ fontWeight: 800, color: T.text }}>通知</span><span style={{ width: 24 }} />
        </div>
        <div style={{ flex: 1, overflow: 'auto' }}>
          {app.notifs.items.length === 0 && <div style={{ padding: 30, textAlign: 'center', color: T.faint }}>通知はまだありません。</div>}
          {app.notifs.items.map((n) => {
            const toPost = ['reply', 'reaction'].includes(n.kind) && n.target_id;
            const open = () => { app.closeNotifs(); if (toPost) app.go('detail', { id: n.target_id }); };
            return (
              <button key={n.id} onClick={open} disabled={!toPost} style={{ display: 'flex', gap: 10, padding: '14px 16px', width: '100%', textAlign: 'left', border: 'none', borderBottom: `1px solid ${T.border}`, background: n.read ? 'none' : `${T.accent}11`, cursor: toPost ? 'pointer' : 'default', font: 'inherit' }}>
                <span style={{ color: T.accent, marginTop: 2 }}><window.Icon.sparkle size={16} /></span>
                <div style={{ flex: 1, minWidth: 0 }}><div style={{ color: T.text, fontSize: 14 }}><b>{n.actor_name || ''}</b>{label(n.kind)}</div><div style={{ color: T.faint, fontSize: 12 }}>{n.group_name}{toPost ? ' · タップで開く' : ''}</div></div>
              </button>
            );
          })}
        </div>
      </div>
    );
  }

  function ExamOverlay({ app }) {
    const T = app.theme;
    return (
      <div style={{ position: 'absolute', inset: 0, background: T.bg, zIndex: 82, display: 'flex', flexDirection: 'column' }}>
        <div style={{ paddingTop: 54, padding: 'max(54px, env(safe-area-inset-top)) 14px 10px', borderBottom: `1px solid ${T.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <button onClick={() => app.closeExam()} style={{ background: 'none', border: 'none', color: T.text, cursor: 'pointer' }}><window.Icon.close size={24} /></button>
          <span style={{ fontWeight: 800, color: T.text }}>{app.examTitle}</span><span style={{ width: 32 }} />
        </div>
        <div style={{ flex: 1, overflowY: 'auto' }}><window.ExamScreen app={app} /></div>
      </div>
    );
  }

  const btn = (T) => ({ width: '100%', marginTop: 10, padding: '14px', borderRadius: 14, border: 'none', background: T.accent, color: '#fff', fontWeight: 800, fontSize: 15, cursor: 'pointer', fontFamily: 'inherit', boxShadow: `0 6px 18px ${T.accent}44` });
  const fld = (T) => ({ width: '100%', boxSizing: 'border-box', marginTop: 8, padding: '12px 14px', borderRadius: 12, border: `1px solid ${T.border}`, background: T.surface, color: T.text, fontSize: 15, fontFamily: 'inherit', outline: 'none' });

  window.PFApp = App;
})();
