// artsyficial gallery. Buildless React (text/babel), same conventions as orbixcore.ai.
// React hooks are exposed as globals from index.html.

// ============================================================
// ROUTER (path-based, History API)
// ============================================================
function parsePath(p) {
  const parts = (p || '/').replace(/^\/+/, '').replace(/\/+$/, '').split('/');
  if (!parts[0]) return { name: 'home' };
  if (parts[0] === 'archive') return { name: 'archive' };
  if (parts[0] === 'about') return { name: 'about' };
  if (parts[0] === 'week' && /^\d{4}-W\d{2}$/.test(parts[1] || '')) return { name: 'week', key: parts[1] };
  return { name: 'home' };
}
function useRoute() {
  const [route, setRoute] = useState(() => parsePath(window.location.pathname));
  useEffect(() => {
    const onPop = () => {
      setRoute(parsePath(window.location.pathname));
      window.scrollTo({ top: 0, behavior: 'instant' });
    };
    window.addEventListener('popstate', onPop);
    window.addEventListener('art:nav', onPop);
    return () => {
      window.removeEventListener('popstate', onPop);
      window.removeEventListener('art:nav', onPop);
    };
  }, []);
  return route;
}
const go = (path) => {
  if (window.location.pathname !== path) {
    window.history.pushState({}, '', path);
    window.dispatchEvent(new Event('art:nav'));
  }
};
function Link({ to, className, children }) {
  return (
    <a href={to} className={className} onClick={(e) => { if (e.metaKey || e.ctrlKey) return; e.preventDefault(); go(to); }}>
      {children}
    </a>
  );
}

// ============================================================
// DATA
// ============================================================
function useFetch(url) {
  const [state, setState] = useState({ loading: true, data: null, error: null, status: 0 });
  useEffect(() => {
    let cancelled = false;
    setState({ loading: true, data: null, error: null, status: 0 });
    fetch(url)
      .then(async (r) => {
        const body = await r.json().catch(() => null);
        if (cancelled) return;
        if (!r.ok) setState({ loading: false, data: null, error: (body && body.error) || `HTTP ${r.status}`, status: r.status });
        else setState({ loading: false, data: body, error: null, status: r.status });
      })
      .catch((e) => { if (!cancelled) setState({ loading: false, data: null, error: e.message, status: 0 }); });
    return () => { cancelled = true; };
  }, [url]);
  return state;
}

const fmtDate = (iso) => (iso ? new Date(iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : '');
const weekNo = (key) => (key || '').split('-W')[1];

// ============================================================
// SHELL
// ============================================================
function Nav({ theme, onToggleTheme, route }) {
  const links = [
    { to: '/', id: 'home', label: 'This week' },
    { to: '/archive', id: 'archive', label: 'Archive' },
    { to: '/about', id: 'about', label: 'How it works' },
  ];
  const active = route.name === 'week' ? 'archive' : route.name;
  return (
    <nav className="nav">
      <Link to="/" className="brand">
        <img src="/images/mark.svg" alt="" width="26" height="26" />
        <span>artsyficial</span>
      </Link>
      <div className="nav-links">
        {links.map((l) => (
          <Link key={l.id} to={l.to} className={`nav-link ${active === l.id ? 'is-active' : ''}`}>{l.label}</Link>
        ))}
        <button className="theme-toggle" onClick={onToggleTheme} aria-label="Toggle theme" title="Toggle theme">
          {theme === 'dark' ? (
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" /></svg>
          ) : (
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></svg>
          )}
        </button>
      </div>
    </nav>
  );
}

function Footer() {
  return (
    <footer className="footer">
      <div className="footer-inner">
        <div>artsyficial. Every image and every word on this site was generated by AI models running on one computer. Nothing here was made by a person.</div>
        <div><a href="https://orbixcore.ai">An Orbix Core experiment</a></div>
      </div>
    </footer>
  );
}

function PageHeader({ eyebrow, title, soft, body, badge }) {
  return (
    <header className="page-header">
      <div className="section-eyebrow mono"><span className="eyebrow-dot" /> {eyebrow}</div>
      <h1 className="page-title">{title} {soft && <span className="section-title-soft">{soft}</span>}</h1>
      {body && <p className="page-body">{body}</p>}
      {badge && <div className="ai-badge">{badge}</div>}
    </header>
  );
}

// ============================================================
// GALLERY
// ============================================================
function Scores({ v }) {
  return (
    <div className="scores mono">
      <span>orig <b>{v.originality}</b></span>
      <span>emotion <b>{v.emotional_impact}</b></span>
      <span>craft <b>{v.execution}</b></span>
    </div>
  );
}

function Votes({ votes }) {
  if (!votes.length) return <p>No juror scored this work.</p>;
  return votes.map((v) => (
    <div className="vote" key={v.juror_model_id}>
      <div className="vote-head">
        <b>{v.juror_label}</b>
        <span className="vote-scores mono">{v.originality} · {v.emotional_impact} · {v.execution}</span>
      </div>
      <p>{v.rationale}</p>
    </div>
  ));
}

function ArtworkCard({ a, votes }) {
  const img = a.image_url;
  return (
    <article className="card" id={`work-${a.id}`}>
      <figure>
        <a href={img} target="_blank" rel="noopener">
          <img src={img} alt={`${a.title || 'Untitled'} - AI-generated painting by ${a.model_label}`} loading="lazy" decoding="async" width={a.width || 1024} height={a.height || 1024} />
        </a>
        {a.rank ? <span className="rank-pill mono">#{a.rank}</span> : null}
        <span className="kind-pill mono">{a.artist_kind === 'svg' ? 'SVG, drawn in code' : a.artist_kind === 'manual' ? 'cloud model, uploaded by hand' : 'AI image'}</span>
      </figure>
      <div className="card-body">
        <div>
          <h3 className="card-title">{a.title || 'Untitled'}</h3>
          <div className="card-by">
            <b>{a.model_label}</b>
            {a.artist_kind === 'manual' ? <> · {a.painter_label}</> : a.painter_label ? <> · concept by the model, pixels by {a.painter_label}</> : <> · every line written as SVG by the model</>}
          </div>
        </div>
        <div>
          <div className="label mono">Artist statement</div>
          <p>{a.artist_statement}</p>
        </div>
        <div>
          <div className="label mono">Viewer note</div>
          <p className="quote">{a.viewer_note}</p>
        </div>
        <details>
          <summary>Jury ({votes.length} {votes.length === 1 ? 'juror' : 'jurors'})</summary>
          <div className="details-body"><Votes votes={votes} /></div>
        </details>
        <details>
          <summary>Prompt and details</summary>
          <div className="details-body">
            <div>
              <div className="label mono">The exact prompt the artist received</div>
              <pre className="prompt">{a.prompt_used}</pre>
            </div>
            {a.image_prompt && (
              <div>
                <div className="label mono">What the artist asked the painter for</div>
                <pre className="prompt">{a.image_prompt}</pre>
              </div>
            )}
            <div className="meta mono">
              <span>model: {a.model_id}</span>
              {a.seed !== null && a.seed !== undefined ? <span>seed: {a.seed}</span> : null}
              {a.width ? <span>{a.width}x{a.height}</span> : null}
              {a.artist_kind === 'svg' && a.thumb_url ? <a href={a.thumb_url} target="_blank" rel="noopener">PNG render</a> : null}
            </div>
          </div>
        </details>
      </div>
    </article>
  );
}

function Spotlight({ data }) {
  const s = data.summary;
  if (!s || !s.winners || !s.winners.length) return null;
  const byId = Object.fromEntries(data.artworks.map((a) => [a.id, a]));
  const medals = ['1st', '2nd', '3rd'];
  return (
    <section className="spotlight">
      <div className="spotlight-head">
        <div>
          <div className="section-eyebrow mono"><span className="eyebrow-dot" /> Jury verdict</div>
          <h2>This week's winners</h2>
        </div>
        <div className="summary-by mono">summary written by {s.head_juror_label}</div>
      </div>
      <p className="summary">{s.summary_text}</p>
      <div className="winners">
        {s.winners.map((w, i) => {
          const a = byId[w.artwork_id];
          if (!a) return null;
          return (
            <a className="winner" key={w.artwork_id} href={`#work-${a.id}`}>
              <figure>
                <img src={a.thumb_url || a.image_url} alt={`${a.title} by ${a.model_label}`} loading="lazy" decoding="async" />
                <span className="medal mono">{medals[i] || `#${w.rank}`}</span>
              </figure>
              <div className="winner-body">
                <h3 className="winner-title">{a.title}</h3>
                <div className="winner-by">{a.model_label}</div>
                <Scores v={{ originality: w.mean_originality.toFixed(1), emotional_impact: w.mean_emotional_impact.toFixed(1), execution: w.mean_execution.toFixed(1) }} />
              </div>
            </a>
          );
        })}
      </div>
    </section>
  );
}

function WeekView({ data }) {
  const votesFor = (id) => data.votes.filter((v) => v.artwork_id === id);
  const w = data.week;
  return (
    <article className="page">
      <PageHeader
        eyebrow={`Week ${weekNo(w.key)} · ${w.range}${w.dry_run ? ' · dry run' : ''}`}
        title="The gallery"
        soft={`of week ${weekNo(w.key)}.`}
        body={`${data.artworks.length} AI models each got a blank canvas and complete freedom. They painted, wrote a statement, and a jury of AI models judged the result.`}
        badge="Everything on this page is AI-generated"
      />
      <Spotlight data={data} />
      <section className="grid">
        {data.artworks.map((a) => <ArtworkCard key={a.id} a={a} votes={votesFor(a.id)} />)}
      </section>
    </article>
  );
}

function StatusNotice() {
  const st = useFetch('/api/gallery/status');
  const w = st.data && st.data.week;
  let text = 'Nothing has been published yet. The first gallery appears once a weekly run completes.';
  if (w && w.status !== 'published') {
    const done = (st.data.artworks && st.data.artworks.done) || 0;
    const map = {
      pending: `Week ${weekNo(w.key)} is on the calendar; the studio has not opened yet.`,
      painting: `Week ${weekNo(w.key)} is being painted right now. ${done} work${done === 1 ? '' : 's'} finished so far.`,
      judging: `Week ${weekNo(w.key)}: the paintings are done and the jury is deliberating.`,
      failed: `Week ${weekNo(w.key)} hit a problem and will be retried.`,
      missed: `Week ${weekNo(w.key)} was missed; the studio computer was off.`,
    };
    text = map[w.status] || text;
  }
  return <div className="notice"><strong>No gallery to show yet.</strong> {text}</div>;
}

function GalleryPage({ weekKey }) {
  const url = weekKey ? `/api/gallery/week?key=${encodeURIComponent(weekKey)}` : '/api/gallery/current';
  const { loading, data, error, status } = useFetch(url);
  if (loading) return <article className="page"><PageHeader eyebrow="artsyficial" title="Loading" soft="the gallery." /><div className="skeleton" /></article>;
  if (error) {
    return (
      <article className="page">
        <PageHeader eyebrow="artsyficial" title="A weekly gallery" soft="painted by machines." body="Every week, several AI models each paint whatever they want, write about it, and a jury of AI models picks the winners." badge="Everything on this site is AI-generated" />
        {status === 404 ? <StatusNotice /> : <div className="notice"><strong>Could not load the gallery.</strong> {error}</div>}
      </article>
    );
  }
  return <WeekView data={data} />;
}

// ============================================================
// ARCHIVE
// ============================================================
function ArchivePage() {
  const { loading, data, error } = useFetch('/api/gallery/archive');
  return (
    <article className="page">
      <PageHeader eyebrow="Archive" title="Past" soft="weeks." body="Every published week, newest first. Click one to see all its works and the jury's reasoning." />
      {loading && <div className="skeleton" />}
      {error && <div className="notice">Could not load the archive. {error}</div>}
      {data && !data.weeks.length && <div className="notice">No weeks published yet.</div>}
      {data && (
        <section className="archive">
          {data.weeks.map((w) => (
            <Link key={w.key} to={`/week/${w.key}`} className="archive-card">
              <figure>{w.winner && <img src={w.winner.thumb_url || w.winner.image_url} alt={`${w.winner.title} by ${w.winner.model_label}`} loading="lazy" decoding="async" />}</figure>
              <div className="archive-body">
                <div className="archive-week mono">Week {weekNo(w.key)} · {w.range}</div>
                <h3 className="archive-title">{w.winner ? w.winner.title : 'No winner'}</h3>
                <div className="archive-sub">{w.winner ? `by ${w.winner.model_label} · ` : ''}{w.artwork_count} works</div>
              </div>
            </Link>
          ))}
        </section>
      )}
    </article>
  );
}

// ============================================================
// ABOUT
// ============================================================
function AboutPage() {
  const { data } = useFetch('/api/gallery/prompts');
  return (
    <article className="page">
      <PageHeader eyebrow="How it works" title="A weekly exhibition" soft="with no humans in the loop." badge="Everything on this site is AI-generated" />
      <div className="prose">
        <p>Once a week, a handful of open AI models running on a single computer each receive the same short prompt: a blank canvas and complete creative freedom. Each one decides what to paint, describes it for a text-to-image model (or draws it directly as SVG code), and writes an artist statement and a note on what the viewer should feel.</p>
        <p>Then a jury of vision-capable models looks at the anonymised entries in a shuffled order and scores originality, emotional impact and execution. A juror never scores work made by its own model family. The scores are averaged in plain code, and one head juror writes the summary you see on the gallery page from the jurors' rationales.</p>
        <p>Every image and every text is checked by a moderation model before it is published. Anything flagged is hidden, not deleted. All of it runs on local hardware with llama.cpp and stable-diffusion.cpp; no cloud model is involved.</p>
        <h2>The artist prompt</h2>
        <p>{data ? data.notes.artist : ''}</p>
        {data && <pre className="prompt">{data.artist_image}</pre>}
        <h2>The code-painter prompt</h2>
        {data && <pre className="prompt">{data.artist_svg}</pre>}
        <h2>The juror prompt</h2>
        <p>{data ? data.notes.jury : ''}</p>
        {data && <pre className="prompt">{data.juror}</pre>}
        <h2>The head juror prompt</h2>
        {data && <pre className="prompt">{data.head_juror}</pre>}
        <h2>The moderation prompt</h2>
        {data && <pre className="prompt">{data.moderation}</pre>}
      </div>
    </article>
  );
}

// ============================================================
// APP
// ============================================================
function App() {
  const [theme, setTheme] = useState(() => {
    try { return localStorage.getItem('orbix-theme') || 'dark'; } catch (e) { return 'dark'; }
  });
  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    try { localStorage.setItem('orbix-theme', theme); } catch (e) {}
  }, [theme]);
  const route = useRoute();
  const page = (() => {
    switch (route.name) {
      case 'archive': return <ArchivePage />;
      case 'about': return <AboutPage />;
      case 'week': return <GalleryPage weekKey={route.key} />;
      default: return <GalleryPage />;
    }
  })();
  return (
    <>
      <Nav theme={theme} onToggleTheme={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))} route={route} />
      <main key={route.name + (route.key || '')}>{page}</main>
      <Footer />
    </>
  );
}

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