/* global React, ReactDOM */
const { useState, useEffect, useMemo, useRef } = React;

// ---------- Utilities ----------
const fmtDate = (iso) => {
  const d = new Date(iso + "T00:00:00");
  return d.toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric" });
};
const fmtDateShort = (iso) => {
  const d = new Date(iso + "T00:00:00");
  return d.toLocaleDateString("en-US", { month: "short", day: "2-digit" });
};
const slugify = (s) => s.toLowerCase().replace(/[^\w]+/g, "-").replace(/^-|-$/g, "");

// ---------- Routing ----------
// Supports BOTH hash routes (for SPA nav) and direct Jekyll URLs like /posts/slug/
const parseLocation = () => {
  // Prefer hash route if present
  if (window.location.hash && window.location.hash.length > 1) {
    const h = window.location.hash.replace(/^#/, "");
    const parts = h.split("/").filter(Boolean);
    if (parts.length === 0) return { name: "home" };
    if (parts[0] === "post" && parts[1]) return { name: "post", slug: decodeURIComponent(parts[1]) };
    if (parts[0] === "tag" && parts[1]) return { name: "tag", value: decodeURIComponent(parts[1]) };
    if (parts[0] === "category" && parts[1]) return { name: "category", value: decodeURIComponent(parts[1]) };
    return { name: parts[0] };
  }
  // Fall back to Jekyll path
  const path = window.location.pathname.replace(/^\/+|\/+$/g, "");
  const segs = path.split("/").filter(Boolean);
  if (segs.length === 0) return { name: "home" };
  if (segs[0] === "posts" && segs[1]) return { name: "post", slug: segs[1] };
  if (segs[0] === "about") return { name: "about" };
  if (segs[0] === "archives") return { name: "archives" };
  if (segs[0] === "categories") return { name: "categories" };
  if (segs[0] === "tags") return { name: "tags" };
  return { name: "home" };
};
const navigate = (hash) => { window.location.hash = hash; };

// ---------- Icons ----------
const Icon = ({ name, size = 14 }) => {
  const c = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round" };
  if (name === "github") return <svg {...c}><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 6.77 5.07 5.07 0 0 0 19.91 3S18.73 2.65 16 4.55a13.38 13.38 0 0 0-7 0C6.27 2.65 5.09 3 5.09 3A5.07 5.07 0 0 0 5 6.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 20.13V24"/></svg>;
  if (name === "linkedin") return <svg {...c}><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/><path d="M22 21v-7a4 4 0 0 0-8 0v7"/><path d="M10 9v12"/></svg>;
  if (name === "rss") return <svg {...c}><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1.5"/></svg>;
  if (name === "sun") return <svg {...c}><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>;
  if (name === "moon") return <svg {...c}><path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"/></svg>;
  if (name === "play") return <svg {...c} fill="currentColor" stroke="none"><path d="M7 4.5v15l13-7.5L7 4.5z"/></svg>;
  if (name === "pause") return <svg {...c} fill="currentColor" stroke="none"><rect x="6" y="4.5" width="4" height="15" rx="1"/><rect x="14" y="4.5" width="4" height="15" rx="1"/></svg>;
  if (name === "stop") return <svg {...c} fill="currentColor" stroke="none"><rect x="5" y="5" width="14" height="14" rx="2"/></svg>;
  return null;
};

// ---------- Sidebar ----------
const Sidebar = ({ route, data }) => {
  const allTags = useMemo(() => {
    const counts = {};
    data.posts.forEach(p => (p.tags || []).forEach(t => counts[t] = (counts[t] || 0) + 1));
    return Object.entries(counts).sort((a, b) => b[1] - a[1]);
  }, [data]);

  const items = [
    { key: "home", label: "home", path: "/home", sc: "h" },
    { key: "archives", label: "archives", path: "/archives", sc: "a" },
    { key: "categories", label: "categories", path: "/categories", sc: "c" },
    { key: "tags", label: "tags", path: "/tags", sc: "t" },
    { key: "about", label: "about", path: "/about", sc: "/" },
  ];
  const activeTag = route.name === "tag" ? route.value : null;

  const isLight = document.documentElement.classList.contains("light");
  const toggleTheme = () => {
    document.documentElement.classList.toggle("light");
    localStorage.setItem("theme", document.documentElement.classList.contains("light") ? "light" : "dark");
  };

  return (
    <aside className="sidebar">
      <div className="brand" onClick={() => navigate("/home")} style={{ cursor: "pointer" }}>
        <div className="avatar">0x</div>
        <div>
          <div className="brand-name"><span className="prompt">~</span><span>{data.site.title}</span></div>
          <div className="brand-tag">security · AI · projects</div>
        </div>
      </div>

      <nav className="nav">
        {items.map(n => (
          <div key={n.key}
               className={"nav-item " + (route.name === n.key ? "active" : "")}
               onClick={() => navigate(n.path)}>
            <span className="nav-dot"></span>
            <span>{n.label}</span>
            <span className="nav-key">{n.sc}</span>
          </div>
        ))}
      </nav>

      {allTags.length > 0 && (
        <div>
          <div className="sidebar-section-title">Top tags</div>
          <div className="tags-cloud">
            {allTags.slice(0, 10).map(([t, n]) => (
              <span key={t}
                    className={"tag-chip " + (activeTag === t ? "active" : "")}
                    onClick={() => navigate("/tag/" + encodeURIComponent(t))}>
                {t} <span style={{ opacity: .5 }}>{n}</span>
              </span>
            ))}
          </div>
        </div>
      )}

      <div className="sidebar-footer">
        <button className="theme-toggle" onClick={toggleTheme}>
          <Icon name={isLight ? "sun" : "moon"} size={12} />
          <span>{isLight ? "light" : "dark"} mode</span>
        </button>
        <div className="social-row">
          {(data.social || []).map(s => (
            <a key={s.type} className="social-btn" href={s.url} target="_blank" rel="noreferrer" title={s.label}>
              <Icon name={s.type} size={13} />
            </a>
          ))}
        </div>
        <div className="footer-meta">
          © {new Date().getFullYear()} {data.site.author}<br/>
          built with <span style={{ color: "var(--accent)" }}>care</span> <span className="blink"></span>
        </div>
      </div>
    </aside>
  );
};

// ---------- Topbar ----------
const Topbar = ({ route, post }) => {
  const crumbs = [{ label: "~/", to: "/home" }];
  if (route.name === "post" && post) {
    crumbs.push({ label: "posts", to: "/home" },
                { label: post.title.toLowerCase().slice(0, 32) + (post.title.length > 32 ? "…" : ""), to: null });
  } else if (route.name === "tag") crumbs.push({ label: "tags", to: "/tags" }, { label: "#" + route.value, to: null });
  else if (route.name === "category") crumbs.push({ label: "categories", to: "/categories" }, { label: route.value, to: null });
  else if (route.name !== "home") crumbs.push({ label: route.name, to: null });

  const now = new Date().toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
  return (
    <div className="topbar">
      <div className="breadcrumb">
        {crumbs.map((c, i) => (
          <React.Fragment key={i}>
            {i > 0 && <span className="sep">/</span>}
            {c.to ? <span className="crumb-link" onClick={() => navigate(c.to)}>{c.label}</span> : <span>{c.label}</span>}
          </React.Fragment>
        ))}
      </div>
      <div className="topbar-right">
        <span className="status-dot"></span>
        <span>online</span>
        <span style={{ opacity: .5 }}>·</span>
        <span>{now}</span>
      </div>
    </div>
  );
};

// ---------- Post card ----------
const PostCard = ({ post }) => {
  const d = new Date(post.date + "T00:00:00");
  return (
    <article className="post-card" onClick={() => navigate("/post/" + post.slug)}>
      <div className="post-date">
        <span className="year">{d.getFullYear()}</span>
        {d.toLocaleDateString("en-US", { month: "short", day: "2-digit" })}
      </div>
      <div className="post-body-col">
        <div className="post-cats">
          {(post.categories || []).map(c => <span key={c} className="cat-pill">{c}</span>)}
        </div>
        <h2 className="post-title">{post.title}</h2>
        <p className="post-excerpt">{post.excerpt}</p>
        <div className="post-tags">
          {(post.tags || []).slice(0, 5).map(t => <span key={t} className="tag">{t}</span>)}
        </div>
      </div>
      <div className="post-arrow-col">
        <span className="post-arrow">↗</span>
        <span className="post-read">{post.read_minutes} min</span>
      </div>
    </article>
  );
};

// ---------- Home / filtered feed ----------
const Home = ({ data, filter }) => {
  let shown = data.posts;
  if (filter?.kind === "tag") shown = shown.filter(p => (p.tags || []).includes(filter.value));
  if (filter?.kind === "category") shown = shown.filter(p => (p.categories || []).includes(filter.value));

  return (
    <>
      {!filter && (
        <section className="hero">
          <h1>Cloud security, <em>AI exploration</em>, and the occasional deep dive into how systems actually work.</h1>
          <p className="hero-sub">
            {"// " + (data.site.description || "Notes from Champ.")}
          </p>
          <div className="hero-meta">
            <span><span className="dot">●</span> {data.posts.length} post{data.posts.length === 1 ? "" : "s"}</span>
            {data.posts[0] && <span>last shipped · {fmtDate(data.posts[0].date)}</span>}
            <span>reading · ~{data.posts.reduce((s, p) => s + (p.read_minutes || 0), 0)} min total</span>
          </div>
        </section>
      )}
      {filter && (
        <div className="filter-banner">
          <span>Filtering by <strong>{filter.kind === "tag" ? "#" + filter.value : filter.value}</strong> · {shown.length} post{shown.length === 1 ? "" : "s"}</span>
          <span className="clear" onClick={() => navigate("/home")}>clear ✕</span>
        </div>
      )}
      <div className="section-label">{filter ? "matching posts" : "recent posts"}</div>
      <div className="post-list">
        {shown.map(p => <PostCard key={p.slug} post={p} />)}
        {shown.length === 0 && (
          <div style={{ padding: "40px 0", fontFamily: "var(--mono)", color: "var(--text-3)" }}>
            No posts yet. Add a markdown file to <code>_posts/</code>.
          </div>
        )}
      </div>
    </>
  );
};

// ---------- Post detail ----------
// Key hybrid move: the post body HTML is Jekyll-rendered (kramdown + rouge),
// we just inject it and enhance with TOC, reading progress, etc.
const PostDetail = ({ post, data }) => {
  const articleRef = useRef(null);
  const bodyRef = useRef(null);
  const [toc, setToc] = useState([]);
  const [activeH, setActiveH] = useState(null);
  const [progress, setProgress] = useState(0);
  const [ttsState, setTtsState] = useState("idle"); // idle | playing | paused
  const ttsSupported = typeof window !== "undefined" && "speechSynthesis" in window;
  const ttsChunksRef = useRef([]);
  const ttsIdxRef = useRef(0);

  const buildSpeechText = () => {
    if (!bodyRef.current) return "";
    const clone = bodyRef.current.cloneNode(true);
    // Strip code blocks, inline code, and figure captions — TTS reading code is unpleasant
    clone.querySelectorAll("div.highlight, figure.highlight, pre, code, figcaption").forEach(el => el.remove());
    const text = (post.title + ". ") + clone.innerText.replace(/\s+/g, " ").trim();
    return text;
  };

  const speakNext = () => {
    if (ttsIdxRef.current >= ttsChunksRef.current.length) {
      setTtsState("idle");
      return;
    }
    const u = new SpeechSynthesisUtterance(ttsChunksRef.current[ttsIdxRef.current]);
    u.rate = 1.0;
    u.onend = () => {
      ttsIdxRef.current += 1;
      speakNext();
    };
    u.onerror = () => {
      setTtsState("idle");
    };
    window.speechSynthesis.speak(u);
  };

  const handleTTS = () => {
    if (!ttsSupported) return;
    if (ttsState === "playing") {
      window.speechSynthesis.pause();
      setTtsState("paused");
      return;
    }
    if (ttsState === "paused") {
      window.speechSynthesis.resume();
      setTtsState("playing");
      return;
    }
    // idle: start fresh
    window.speechSynthesis.cancel();
    const text = buildSpeechText();
    // Split into sentences to sidestep Chrome's ~200-char utterance cutoff bug
    ttsChunksRef.current = text.match(/[^.!?]+[.!?]+\s*|[^.!?]+$/g) || [text];
    ttsIdxRef.current = 0;
    setTtsState("playing");
    speakNext();
  };

  const stopTTS = () => {
    if (!ttsSupported) return;
    window.speechSynthesis.cancel();
    ttsChunksRef.current = [];
    ttsIdxRef.current = 0;
    setTtsState("idle");
  };

  // Stop speech when leaving the post (slug change or unmount)
  useEffect(() => {
    return () => stopTTS();
  }, [post.slug]);

  // After Jekyll HTML is injected, scan for h2/h3 to build TOC and add IDs
  useEffect(() => {
    if (!bodyRef.current) return;
    window.scrollTo({ top: 0, behavior: "instant" });
    const headings = [];
    bodyRef.current.querySelectorAll("h2, h3").forEach(h => {
      if (!h.id) h.id = slugify(h.textContent);
      headings.push({ id: h.id, text: h.textContent, level: h.tagName.toLowerCase(), el: h });
    });
    setToc(headings);

    // Upgrade code blocks: add a header with lang + copy button
    bodyRef.current.querySelectorAll("div.highlight, figure.highlight").forEach(hl => {
      if (hl.querySelector(".code-head")) return;
      const pre = hl.querySelector("pre");
      if (!pre) return;
      const lang = hl.className.match(/language-(\w+)/)?.[1] ||
                   hl.closest("[class*='language-']")?.className.match(/language-(\w+)/)?.[1] ||
                   "shell";
      const head = document.createElement("div");
      head.className = "code-head";
      head.innerHTML = `<span class="lang">${lang}</span><span class="copy">copy</span>`;
      hl.insertBefore(head, hl.firstChild);
      head.querySelector(".copy").addEventListener("click", (e) => {
        const text = pre.innerText;
        navigator.clipboard?.writeText(text);
        e.target.textContent = "copied ✓";
        setTimeout(() => { e.target.textContent = "copy"; }, 1200);
      });
    });

    // Intercept internal links to other posts so SPA nav kicks in
    bodyRef.current.querySelectorAll("a[href]").forEach(a => {
      const href = a.getAttribute("href");
      if (href && href.startsWith("/posts/")) {
        a.addEventListener("click", (e) => {
          e.preventDefault();
          const slug = href.replace(/^\/posts\/|\/$/g, "");
          navigate("/post/" + slug);
        });
      }
    });
  }, [post]);

  useEffect(() => {
    const onScroll = () => {
      const el = articleRef.current;
      if (el) {
        const rect = el.getBoundingClientRect();
        const vh = window.innerHeight;
        const total = rect.height - vh;
        const scrolled = Math.max(0, Math.min(total, -rect.top));
        setProgress(total > 0 ? Math.round((scrolled / total) * 100) : 0);
      }
      let current = null;
      for (const t of toc) {
        const top = t.el.getBoundingClientRect().top;
        if (top < 120) current = t.id;
      }
      setActiveH(current || toc[0]?.id);
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [toc]);

  const idx = data.posts.findIndex(p => p.slug === post.slug);
  const prev = data.posts[idx + 1];
  const next = data.posts[idx - 1];

  return (
    <div className="post-layout">
      <article className="article" ref={articleRef}>
        <header className="article-header">
          <div className="article-cats">
            {(post.categories || []).map(c => (
              <span key={c} className="cat-pill" style={{ cursor: "pointer" }}
                    onClick={() => navigate("/category/" + encodeURIComponent(c))}>{c}</span>
            ))}
          </div>
          <h1 className="article-title">{post.title}</h1>
          <div className="article-meta">
            <span>Posted · <strong>{fmtDate(post.date)}</strong></span>
            <span>Reading · <strong>{post.read_minutes} min</strong></span>
            <span>Author · <strong>{data.site.author}</strong></span>
            <span>Words · <strong>{(post.word_count || 0).toLocaleString()}</strong></span>
          </div>
          {ttsSupported && (
            <div className="article-actions">
              <button
                className={"tts-btn" + (ttsState !== "idle" ? " active" : "")}
                onClick={handleTTS}
                aria-label={ttsState === "playing" ? "Pause" : ttsState === "paused" ? "Resume" : "Listen to post"}>
                <Icon name={ttsState === "playing" ? "pause" : "play"} size={12} />
                <span>{ttsState === "idle" ? "Listen" : ttsState === "playing" ? "Pause" : "Resume"}</span>
              </button>
              {ttsState !== "idle" && (
                <button className="tts-btn tts-stop" onClick={stopTTS} aria-label="Stop">
                  <Icon name="stop" size={10} />
                </button>
              )}
            </div>
          )}
        </header>

        {post.image?.path && (
          <img className="post-cover" src={post.image.path}
               alt={post.image.alt || post.title}
               loading="lazy" />
        )}

        <div className="article-body" ref={bodyRef} dangerouslySetInnerHTML={{ __html: post.html }} />

        <footer className="article-footer">
          <div className="article-tags">
            {(post.tags || []).map(t => (
              <span key={t} className="article-tag" onClick={() => navigate("/tag/" + encodeURIComponent(t))}>#{t}</span>
            ))}
          </div>

          <div className="author-card">
            <div className="avatar">0x</div>
            <div>
              <div className="name">{data.site.author}</div>
              <div className="bio">Cybersecurity Engineer. Writes about what breaks and what to do about it.</div>
            </div>
            <button className="follow" onClick={() => navigate("/about")}>About →</button>
          </div>

          <div className="next-prev">
            {prev ? (
              <div className="np-card" onClick={() => navigate("/post/" + prev.slug)}>
                <div className="np-label">← older</div>
                <div className="np-title">{prev.title}</div>
              </div>
            ) : <div></div>}
            {next ? (
              <div className="np-card right" onClick={() => navigate("/post/" + next.slug)}>
                <div className="np-label">newer →</div>
                <div className="np-title">{next.title}</div>
              </div>
            ) : <div></div>}
          </div>
        </footer>
      </article>

      <aside className="toc">
        <div className="toc-title">On this page</div>
        <ul className="toc-list">
          {toc.map(t => (
            <li key={t.id}
                className={"toc-item " + t.level + " " + (activeH === t.id ? "active" : "")}
                onClick={() => window.scrollTo({ top: t.el.getBoundingClientRect().top + window.scrollY - 24, behavior: "smooth" })}>
              {t.text}
            </li>
          ))}
          {toc.length === 0 && <li style={{ color: "var(--text-3)", fontSize: 12 }}>No sections</li>}
        </ul>
        <div className="reading-progress">
          <div>reading · {progress}%</div>
          <div className="progress-bar"><div className="progress-fill" style={{ width: progress + "%" }}></div></div>
        </div>
      </aside>
    </div>
  );
};

// ---------- About ----------
const About = ({ data }) => (
  <>
    <div className="page-head">
      <h1 className="page-title">About</h1>
      <div className="page-sub">// whoami</div>
    </div>
    <div className="about-grid">
      <div className="about-prose">
        {data.about_html
          ? <div dangerouslySetInnerHTML={{ __html: data.about_html }} />
          : (data.about || "").split(/\n\s*\n/).map((para, i) => <p key={i}>{para}</p>)}
      </div>
      <div className="about-card">
        <div className="about-row"><span className="k">name</span><span className="v">{data.site.author}</span></div>
        <div className="about-row"><span className="k">role</span><span className="v">Cybersecurity Engineer</span></div>
        <div className="about-row"><span className="k">studying</span><span className="v">SANS · Cloud Sec</span></div>
        <div className="about-row"><span className="k">posts</span><span className="v">{data.posts.length}</span></div>
        <div style={{ borderTop: "1px dashed var(--line-2)", paddingTop: 12, marginTop: 4 }}>
          <div style={{ color: "var(--text-3)", fontSize: 11, marginBottom: 8 }}>elsewhere</div>
          {(data.social || []).map(s => (
            <div key={s.type} style={{ display: "flex", justifyContent: "space-between", fontSize: 12, padding: "3px 0" }}>
              <span style={{ color: "var(--text-3)" }}>{s.type}</span>
              <span style={{ color: "var(--accent)" }}>{s.handle}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  </>
);

// ---------- Archives ----------
const Archives = ({ data }) => {
  const byYear = useMemo(() => {
    const map = {};
    data.posts.forEach(p => {
      const y = p.date.slice(0, 4);
      (map[y] = map[y] || []).push(p);
    });
    return Object.entries(map).sort((a, b) => b[0].localeCompare(a[0]));
  }, [data]);
  return (
    <>
      <div className="page-head">
        <h1 className="page-title">Archives</h1>
        <div className="page-sub">// {data.posts.length} posts · {byYear.length} years</div>
      </div>
      {byYear.map(([year, items]) => (
        <div key={year} className="archive-year">
          <div className="year-label">{year}</div>
          <div className="year-posts">
            {items.map(p => (
              <div key={p.slug} className="archive-row" onClick={() => navigate("/post/" + p.slug)}>
                <div className="date">{fmtDateShort(p.date)}</div>
                <div className="title">{p.title}</div>
              </div>
            ))}
          </div>
        </div>
      ))}
    </>
  );
};

// ---------- Categories ----------
const Categories = ({ data }) => {
  const byCat = useMemo(() => {
    const map = {};
    data.posts.forEach(p => (p.categories || []).forEach(c => { (map[c] = map[c] || []).push(p); }));
    return Object.entries(map).sort((a, b) => b[1].length - a[1].length);
  }, [data]);
  return (
    <>
      <div className="page-head">
        <h1 className="page-title">Categories</h1>
        <div className="page-sub">// {byCat.length} categories</div>
      </div>
      <div className="grid-cards">
        {byCat.map(([c, items]) => (
          <div key={c} className="cat-card" onClick={() => navigate("/category/" + encodeURIComponent(c))}>
            <div className="count"><strong>{items.length}</strong> post{items.length === 1 ? "" : "s"}</div>
            <div className="name">{c}</div>
            <div className="tags-inline">
              latest · {items[0].title.slice(0, 48)}{items[0].title.length > 48 ? "…" : ""}
            </div>
          </div>
        ))}
      </div>
    </>
  );
};

// ---------- Tags ----------
const Tags = ({ data }) => {
  const byTag = useMemo(() => {
    const counts = {};
    data.posts.forEach(p => (p.tags || []).forEach(t => counts[t] = (counts[t] || 0) + 1));
    return Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
  }, [data]);
  return (
    <div className="tags-page">
      <div className="page-head">
        <h1 className="page-title">Tags</h1>
        <div className="page-sub">// {byTag.length} tags across {data.posts.length} posts</div>
      </div>
      {byTag.map(([t, n]) => (
        <span key={t} className="tag-big" onClick={() => navigate("/tag/" + encodeURIComponent(t))}
              style={{ fontSize: 11 + Math.min(n, 3) * 2 + "px" }}>
          #{t} <span className="n">{n}</span>
        </span>
      ))}
    </div>
  );
};

// ---------- Tweaks ----------
const TWEAK_DEFAULTS = {
  "accentHue": 160,
  "serifHeadings": true,
  "showTOC": true
};

const Tweaks = ({ tweaks, setTweaks }) => {
  const hues = [
    { name: "mint", v: 160 },
    { name: "grass", v: 145 },
    { name: "teal", v: 175 },
    { name: "amber", v: 65 },
    { name: "sky", v: 230 },
  ];
  return (
    <div className="tweaks">
      <h4>Tweaks</h4>
      <div className="tweak-row">
        <label>Accent</label>
        <div className="tweak-swatches">
          {hues.map(h => (
            <div key={h.v}
                 className={"tweak-sw " + (tweaks.accentHue === h.v ? "active" : "")}
                 title={h.name}
                 style={{ background: `oklch(0.78 0.14 ${h.v})` }}
                 onClick={() => setTweaks({ ...tweaks, accentHue: h.v })} />
          ))}
        </div>
      </div>
      <div className="tweak-row">
        <label>Serif headings</label>
        <span className={"tweak-toggle " + (tweaks.serifHeadings ? "on" : "")}
              onClick={() => setTweaks({ ...tweaks, serifHeadings: !tweaks.serifHeadings })}>
          {tweaks.serifHeadings ? "on" : "off"}
        </span>
      </div>
      <div className="tweak-row">
        <label>Show TOC</label>
        <span className={"tweak-toggle " + (tweaks.showTOC ? "on" : "")}
              onClick={() => setTweaks({ ...tweaks, showTOC: !tweaks.showTOC })}>
          {tweaks.showTOC ? "on" : "off"}
        </span>
      </div>
    </div>
  );
};

// ---------- App ----------
const App = () => {
  const [route, setRoute] = useState(parseLocation());
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [tweaks, setTweaks] = useState(TWEAK_DEFAULTS);
  const [editMode, setEditMode] = useState(false);

  // Fetch posts.json (Jekyll-generated)
  useEffect(() => {
    const url = window.__JEKYLL__?.postsUrl || "/posts.json";
    fetch(url)
      .then(r => {
        if (!r.ok) throw new Error("HTTP " + r.status);
        return r.json();
      })
      .then(setData)
      .catch(e => setError(e.message));
  }, []);

  // Route listener
  useEffect(() => {
    const onHash = () => setRoute(parseLocation());
    window.addEventListener("hashchange", onHash);
    window.addEventListener("popstate", onHash);
    return () => {
      window.removeEventListener("hashchange", onHash);
      window.removeEventListener("popstate", onHash);
    };
  }, []);

  // Theme
  useEffect(() => {
    if (localStorage.getItem("theme") === "light") document.documentElement.classList.add("light");
  }, []);

  // Apply tweaks
  useEffect(() => {
    document.documentElement.style.setProperty("--accent", `oklch(0.78 0.14 ${tweaks.accentHue})`);
    if (!tweaks.serifHeadings) {
      document.documentElement.style.setProperty("--serif", `"Geist", sans-serif`);
    } else {
      document.documentElement.style.removeProperty("--serif");
    }
    document.querySelectorAll(".toc").forEach(el => { el.style.display = tweaks.showTOC ? "" : "none"; });
  }, [tweaks]);

  // Edit mode protocol — only in non-production environments.
  useEffect(() => {
    if (!window.__JEKYLL__?.dev) return;
    const h = (e) => {
      if (!e.data) return;
      if (e.data.type === "__activate_edit_mode") setEditMode(true);
      if (e.data.type === "__deactivate_edit_mode") setEditMode(false);
    };
    window.addEventListener("message", h);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", h);
  }, []);

  const updateTweaks = (next) => {
    setTweaks(next);
    if (window.__JEKYLL__?.dev) {
      window.parent.postMessage({ type: "__edit_mode_set_keys", edits: next }, "*");
    }
  };

  if (error) {
    return (
      <div className="spa-loading" style={{ color: "var(--text-2)" }}>
        <div>
          <div style={{ color: "var(--accent)", marginBottom: 8 }}>× posts.json failed to load</div>
          <div style={{ fontSize: 11, color: "var(--text-3)" }}>{error}</div>
          <div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 8 }}>Run <code>bundle exec jekyll serve</code> and reload.</div>
        </div>
      </div>
    );
  }
  if (!data) {
    return <div className="spa-loading">loading<span className="cursor"></span></div>;
  }

  const post = route.name === "post" ? data.posts.find(p => p.slug === route.slug) : null;
  let filter = null;
  if (route.name === "tag") filter = { kind: "tag", value: route.value };
  if (route.name === "category") filter = { kind: "category", value: route.value };

  return (
    <div className="app">
      <Sidebar route={route} data={data} />
      <main className="main">
        <Topbar route={route} post={post} />
        {route.name === "home" && <Home data={data} />}
        {(route.name === "tag" || route.name === "category") && <Home data={data} filter={filter} />}
        {route.name === "post" && post && <PostDetail post={post} data={data} />}
        {route.name === "post" && !post && (
          <div style={{ padding: 40, fontFamily: "var(--mono)", color: "var(--text-2)" }}>
            404 · post not found. <a style={{ color: "var(--accent)", cursor: "pointer" }} onClick={() => navigate("/home")}>← back home</a>
          </div>
        )}
        {route.name === "about" && <About data={data} />}
        {route.name === "archives" && <Archives data={data} />}
        {route.name === "categories" && <Categories data={data} />}
        {route.name === "tags" && <Tags data={data} />}
      </main>
      {editMode && <Tweaks tweaks={tweaks} setTweaks={updateTweaks} />}
    </div>
  );
};

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