// React app shell: topbar, sidebar, hash router.
// Pages are mounted by page-specific files via window.KOL_PAGES.

const { useState, useEffect, useMemo, useCallback } = React;

window.KOL_PAGES = window.KOL_PAGES || {};

function fmtDate(iso) {
  if (!iso) return "";
  try {
    const d = new Date(iso);
    return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" }).toUpperCase();
  } catch (e) { return iso; }
}
window.fmtDate = fmtDate;

function parseHash() {
  const h = (location.hash || "#/").replace(/^#/, "");
  const parts = h.split("/").filter(Boolean);
  return { parts, raw: h };
}

function useRoute() {
  const [route, setRoute] = useState(parseHash());
  useEffect(() => {
    const on = () => setRoute(parseHash());
    window.addEventListener("hashchange", on);
    return () => window.removeEventListener("hashchange", on);
  }, []);
  return route;
}
window.useRoute = useRoute;

function useTheme() {
  const [theme, setTheme] = useState(() => localStorage.getItem("kol_theme") || "light");
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("kol_theme", theme);
  }, [theme]);
  return [theme, setTheme];
}

function Brand() {
  return (
    <a href="#/" className="brand">
      <span className="brand-mark" aria-hidden="true"></span>
      <span className="brand-meta">
        <span>King's OSINT</span>
        <small>London</small>
      </span>
    </a>
  );
}

function Topbar({ theme, setTheme, search, setSearch }) {
  return (
    <header className="topbar">
      <Brand />
      <div className="topbar-spacer" style={{ flex: "0 0 12px" }}></div>
      <div className="search">
        <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
          <circle cx="7" cy="7" r="5"></circle>
          <path d="m11 11 3 3"></path>
        </svg>
        <input
          placeholder="Search tools, articles, sessions…"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter" && search.trim()) location.hash = "#/search"; }}
        />
        <kbd>/</kbd>
      </div>
      <div className="topbar-spacer"></div>
      <button
        className="icon-btn"
        onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
        title={theme === "dark" ? "Switch to light" : "Switch to dark"}
        aria-label="Toggle theme"
      >
        {theme === "dark" ? (
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4">
            <circle cx="8" cy="8" r="3"></circle>
            <path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.4 1.4M11.6 11.6 13 13M3 13l1.4-1.4M11.6 4.4 13 3"></path>
          </svg>
        ) : (
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4">
            <path d="M13.5 9.5a5.5 5.5 0 1 1-7-7 4.5 4.5 0 0 0 7 7z"></path>
          </svg>
        )}
      </button>
      <button
        className="icon-btn"
        onClick={() => window.__kolLogout && window.__kolLogout()}
        title="Lock and sign out"
        aria-label="Lock"
      >
        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4">
          <rect x="3" y="7" width="10" height="7" rx="1.2"></rect>
          <path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2"></path>
        </svg>
      </button>
    </header>
  );
}

function Sidebar({ route }) {
  const active = route.parts.length === 0 ? "home" : (route.parts[0] === "c" ? route.parts[1] : route.parts[0]);
  const isHome = route.parts.length === 0;
  const [sessionCount, setSessionCount] = useState(
    window.KOL_SESSION_COUNT !== undefined
      ? window.KOL_SESSION_COUNT
      : (window.SESSION_NOTES_DEFAULT || []).length
  );
  useEffect(() => {
    const on = (e) => setSessionCount(e.detail);
    window.addEventListener("kol:sessions-count", on);
    return () => window.removeEventListener("kol:sessions-count", on);
  }, []);

  return (
    <nav className="sidebar">
      <div className="sidebar-section">
        <a href="#/" className={isHome ? "active" : ""}>
          <span aria-hidden="true" style={{ width: 14, color: "var(--ink-3)" }}>◉</span> Home
        </a>
        <a href="#/tools" className={active === "tools" ? "active" : ""}>
          <span aria-hidden="true" style={{ width: 14, color: "var(--ink-3)" }}>◧</span>
          All Tools <span className="num">{(window.TOOLS || []).length}</span>
        </a>
        <a href="#/sessions" className={active === "sessions" ? "active" : ""}>
          <span aria-hidden="true" style={{ width: 14, color: "var(--ink-3)" }}>◔</span>
          Session Notes <span className="num">{sessionCount}</span>
        </a>
        <a href="#/ctfs" className={active === "ctfs" ? "active" : ""}>
          <span aria-hidden="true" style={{ width: 14, color: "var(--ink-3)" }}>◈</span>
          Upcoming CTFs
        </a>
      </div>

      <div className="sidebar-section">
        <div className="sidebar-label">Categories</div>
        {window.CATEGORIES.map((c) => (
          <a
            key={c.id}
            href={`#/c/${c.id}`}
            className={active === c.id ? "active" : ""}
            title={c.summary}
          >
            <span aria-hidden="true" style={{ width: 14, color: "var(--ink-3)" }}>{c.icon}</span>
            {c.title} <span className="num">{c.articles.length}</span>
          </a>
        ))}
      </div>

      <div className="sidebar-section">
        <div className="sidebar-label">Group</div>
        <a href="#/about">
          <span aria-hidden="true" style={{ width: 14, color: "var(--ink-3)" }}>◌</span>
          About
        </a>
      </div>
    </nav>
  );
}

function NotFound() {
  return (
    <div style={{ padding: "60px 0", textAlign: "left", maxWidth: 540 }}>
      <div className="crumbs"><a href="#/">Home</a><span>/</span><span style={{ color: "var(--ink)" }}>Not found</span></div>
      <h1 style={{ fontFamily: "var(--font-serif)", fontWeight: 400, fontSize: 40, margin: "0 0 10px", letterSpacing: "-0.015em" }}>Nothing here.</h1>
      <p style={{ color: "var(--ink-3)" }}>The page you asked for does not exist, or has been moved. Try the sidebar.</p>
    </div>
  );
}

const DEFAULT_ABOUT = {
  title: "About King's OSINT",
  lede: "A small group at King's College London that meets weekly to practise open-source investigation. This site is our shared reference — it exists so that nobody has to re-derive the basics in week three.",
  members: "by invitation",
  meets: "Tuesdays",
  updated: "2026-05-09",
  body: "What we cover: operational security, virtual machines, persona management, structured search, image and video verification, geolocation, and archiving. What we do not cover: anything you would not be comfortable explaining to a journalism ethics board.\n\nTo contribute, use the Edit button above.",
};

function About() {
  const [editing, setEditing] = useState(false);
  const [data, setData] = useState(() => window.KOL_ABOUT || null);
  const [draft, setDraft] = useState({});

  useEffect(() => {
    const on = () => setData(window.KOL_ABOUT || null);
    window.addEventListener("kol:page:about", on);
    return () => window.removeEventListener("kol:page:about", on);
  }, []);

  const active = data || DEFAULT_ABOUT;

  function startEdit() {
    setDraft({ ...DEFAULT_ABOUT, ...active });
    setEditing(true);
  }

  function save() {
    window.KOL_STORE.savePageContent("about", draft);
    setData(draft);
    setEditing(false);
  }

  const renderedBody = useMemo(() => {
    const src = active.body || "";
    if (!src) return "";
    if (window.marked) {
      window.marked.setOptions({ gfm: true, breaks: false });
      return window.marked.parse(src);
    }
    return "<p>" + src.replace(/\n\n/g, "</p><p>") + "</p>";
  }, [active.body]);

  return (
    <article className="article">
      <div className="crumbs"><a href="#/">Home</a><span>/</span><span style={{ color: "var(--ink)" }}>About</span></div>
      <h1>{active.title}</h1>
      <p className="article-lede">{active.lede}</p>
      <div className="article-meta">
        <span><b>Members</b> {active.members}</span>
        <span><b>Meets</b> {active.meets}</span>
        <span><b>Updated</b> {fmtDate(active.updated)}</span>
        <span style={{ marginLeft: "auto" }}>
          <button className="btn ghost" style={{ fontSize: 11, padding: "4px 10px" }} onClick={editing ? () => setEditing(false) : startEdit}>
            {editing ? "Close editor" : "Edit"}
          </button>
        </span>
      </div>
      {editing ? (
        <div className="session-editor" style={{ marginBottom: 28 }}>
          <h3>Editing: About page</h3>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 140px", gap: 10 }}>
            <div className="field">
              <label>Title</label>
              <input value={draft.title || ""} onChange={e => setDraft({ ...draft, title: e.target.value })} />
            </div>
            <div className="field">
              <label>Meets</label>
              <input value={draft.meets || ""} onChange={e => setDraft({ ...draft, meets: e.target.value })} placeholder="e.g. Tuesdays" />
            </div>
            <div className="field">
              <label>Updated</label>
              <input type="date" value={draft.updated || ""} onChange={e => setDraft({ ...draft, updated: e.target.value })} />
            </div>
          </div>
          <div className="field">
            <label>Description (shown under title)</label>
            <input value={draft.lede || ""} onChange={e => setDraft({ ...draft, lede: e.target.value })} />
          </div>
          <div className="field">
            <label>Members</label>
            <input value={draft.members || ""} onChange={e => setDraft({ ...draft, members: e.target.value })} placeholder="e.g. by invitation" />
          </div>
          <div className="field">
            <label>Body — Markdown</label>
            <textarea
              value={draft.body || ""}
              onChange={e => setDraft({ ...draft, body: e.target.value })}
              style={{ minHeight: 200, fontFamily: "var(--font-mono)", fontSize: 13 }}
            />
          </div>
          <div className="row">
            <button className="btn" onClick={save}>Save changes</button>
            <button className="btn ghost" onClick={() => setEditing(false)}>Cancel</button>
          </div>
        </div>
      ) : (
        <div dangerouslySetInnerHTML={{ __html: renderedBody }} />
      )}
    </article>
  );
}

function UpcomingCTFs() {
  return (
    <>
      <div className="crumbs">
        <a href="#/">Home</a><span>/</span>
        <span style={{ color: "var(--ink)" }}>Upcoming CTFs</span>
      </div>
      <header className="tools-head">
        <h1>Upcoming CTFs</h1>
        <p>Competitions and practice events relevant to the group. Check back for updates.</p>
      </header>
      <div className="empty">No upcoming CTFs listed yet.</div>
    </>
  );
}

function SearchResults({ search }) {
  const q = (search || "").trim().toLowerCase();

  const articleMatches = useMemo(() => {
    if (!q) return [];
    const results = [];
    (window.CATEGORIES || []).forEach(cat => {
      (cat.articles || []).forEach(art => {
        const hay = [art.title, art.lede, art.markdown || art.body || "", ...(art.sections?.map(s => s.text) || [])].join(" ").toLowerCase();
        if (hay.includes(q)) results.push({ cat, art });
      });
    });
    return results;
  }, [q]);

  const toolMatches = useMemo(() => {
    if (!q) return [];
    return (window.TOOLS || []).filter(t => {
      const hay = [t.name, t.category, t.desc, ...(t.tags || [])].join(" ").toLowerCase();
      return hay.includes(q);
    });
  }, [q]);

  const sessionMatches = useMemo(() => {
    if (!q) return [];
    const sessions = window.KOL_SESSIONS || window.SESSION_NOTES_DEFAULT || [];
    return sessions.filter(n => {
      const hay = [n.title, n.body, ...(n.tags || [])].join(" ").toLowerCase();
      return hay.includes(q);
    });
  }, [q]);

  const total = articleMatches.length + toolMatches.length + sessionMatches.length;

  return (
    <>
      <div className="crumbs">
        <a href="#/">Home</a><span>/</span>
        <span style={{ color: "var(--ink)" }}>Search</span>
      </div>
      <header className="tools-head">
        <h1>Search results</h1>
        <p style={{ margin: 0, color: "var(--ink-3)" }}>
          {q ? <>{total} result{total !== 1 ? "s" : ""} for <em style={{ color: "var(--ink-2)", fontStyle: "normal", fontWeight: 500 }}>"{q}"</em></> : "Enter a query in the search bar above and press Enter."}
        </p>
      </header>

      {q && total === 0 && (
        <div className="empty">No results for "{q}"</div>
      )}

      {articleMatches.length > 0 && (
        <>
          <div className="section-head" style={{ marginTop: 28 }}>
            <h2>Articles <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-4)", fontWeight: 400 }}>{articleMatches.length}</span></h2>
          </div>
          <div className="cards">
            {articleMatches.map(({ cat, art }) => (
              <a key={art.id} href={`#/c/${cat.id}/${art.id}`} className="card">
                <div className="card-eyebrow">{cat.label} · {cat.title}</div>
                <h3>{art.title}</h3>
                <p>{art.lede}</p>
                <div className="card-foot">
                  <span>UPDATED {fmtDate(art.updated)}</span>
                  <span className="dot"></span>
                  <span>{art.readMin} MIN</span>
                </div>
              </a>
            ))}
          </div>
        </>
      )}

      {toolMatches.length > 0 && (
        <>
          <div className="section-head" style={{ marginTop: 28 }}>
            <h2>Tools <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-4)", fontWeight: 400 }}>{toolMatches.length}</span></h2>
          </div>
          <div className="tools-grid">
            {toolMatches.map(t => (
              <a key={t.name} href={t.url} target="_blank" rel="noopener noreferrer" className="tool" style={{ textDecoration: "none" }}>
                <div className="tool-head">
                  <h4>{t.name}</h4>
                  <span className={`tool-cost${t.cost === "paid" ? " paid" : ""}`}>{t.cost}</span>
                </div>
                <p>{t.desc}</p>
                {t.tags?.length ? (
                  <div className="tool-tags">
                    {t.tags.map(tag => <span key={tag} className="tool-tag">{tag}</span>)}
                  </div>
                ) : null}
              </a>
            ))}
          </div>
        </>
      )}

      {sessionMatches.length > 0 && (
        <>
          <div className="section-head" style={{ marginTop: 28 }}>
            <h2>Session Notes <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-4)", fontWeight: 400 }}>{sessionMatches.length}</span></h2>
          </div>
          {sessionMatches.map(note => (
            <a key={note.n} href="#/sessions" className="card" style={{ display: "block", border: "1px solid var(--rule)", borderRadius: "var(--radius)", padding: "18px 20px", marginBottom: 8 }}>
              <div className="card-eyebrow">SESSION {String(note.n).padStart(2, "0")} · {fmtDate(note.date)}</div>
              <h3 style={{ fontSize: 17, margin: "4px 0 6px" }}>{note.title}</h3>
              <p style={{ fontSize: 14, color: "var(--ink-2)", margin: 0, lineHeight: 1.5 }}>{(note.body || "").slice(0, 200)}{(note.body || "").length > 200 ? "…" : ""}</p>
            </a>
          ))}
        </>
      )}
    </>
  );
}

function Router({ route, search, setSearch }) {
  if (route.parts.length === 0) return <window.KOL_PAGES.Home />;
  const [head, ...rest] = route.parts;
  if (head === "tools") return <window.KOL_PAGES.Tools search={search} setSearch={setSearch} />;
  if (head === "search") return <SearchResults search={search} />;
  if (head === "sessions") return <window.KOL_PAGES.Sessions />;
  if (head === "ctfs") return <UpcomingCTFs />;
  if (head === "about") return <About />;
  if (head === "c") {
    const [catId, articleId] = rest;
    const cat = window.CATEGORIES.find(c => c.id === catId);
    if (!cat) return <NotFound />;
    if (!articleId) return <window.KOL_PAGES.Category cat={cat} />;
    const art = cat.articles.find(a => a.id === articleId);
    if (!art) return <NotFound />;
    return <window.KOL_PAGES.Article cat={cat} article={art} />;
  }
  return <NotFound />;
}

function App() {
  const route = useRoute();
  const [theme, setTheme] = useTheme();
  const [search, setSearch] = useState("");

  // global "/" shortcut focuses the topbar search.
  useEffect(() => {
    const on = (e) => {
      if (e.key === "/" && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA") {
        e.preventDefault();
        document.querySelector(".topbar .search input")?.focus();
      }
    };
    window.addEventListener("keydown", on);
    return () => window.removeEventListener("keydown", on);
  }, []);

  return (
    <div className="app">
      <Topbar theme={theme} setTheme={setTheme} search={search} setSearch={setSearch} />
      <div className="main">
        <Sidebar route={route} />
        <main className="content">
          <Router route={route} search={search} setSearch={setSearch} />
        </main>
      </div>
      {window.KOL_PAGES.TweaksPanel ? <window.KOL_PAGES.TweaksPanel /> : null}
      {window.KOL_PAGES.DeleteModal ? <window.KOL_PAGES.DeleteModal /> : null}
    </div>
  );
}

let __kolMounted = false;
window.__kolMount = function () {
  if (__kolMounted) return;
  __kolMounted = true;
  const root = ReactDOM.createRoot(document.getElementById("app-root"));
  root.render(<App />);
};
window.addEventListener("kol:auth", () => window.__kolMount());
// gate.js sets window.__kolReady = true after KOL_STORE.init() resolves, then
// fires kol:auth. If this script compiled after that event fired, mount directly.
if (window.__kolReady) window.__kolMount();
