// app.jsx — root: routing, opt-in modal, vault transition, persistence, tweaks
const { useState: useS, useEffect: useE, useRef: useR } = React;

const STORE_KEY = "hyro70.member.v1";
const PAGE_KEY = "hyro70.page.v1";

/* ---------- Klaviyo (client-side, public company_id) ---------- */
const KLAVIYO_COMPANY_ID = "XmncDj";
const KLAVIYO_REVISION = "2024-10-15";
const KLAVIYO_LIST_ID = "XVSpd3"; // 70-Day Challenge Participants (entrants)
const KLAVIYO_COMPLETIONS_LIST_ID = "Vpq37B"; // 70-Day Challenge Completions
const KLAVIYO_API = "https://a.klaviyo.com/client";

/* Subscribe a profile to a Klaviyo list (single opt-in) + set props.
   NOTE: do NOT include a `subscriptions` block in profile.attributes here
   (Klaviyo /client/subscriptions rejects it 400). Email alone is enough. */
function klaviyoSubscribeToList(firstName, email, listId, props) {
  const body = {
    data: {
      type: "subscription",
      attributes: {
        custom_source: "challenge.drinkhyro.com.au",
        profile: {
          data: {
            type: "profile",
            attributes: {
              email: email,
              first_name: firstName,
              properties: props || {}
            }
          }
        }
      },
      relationships: { list: { data: { type: "list", id: listId } } }
    }
  };
  return fetch(KLAVIYO_API + "/subscriptions/?company_id=" + encodeURIComponent(KLAVIYO_COMPANY_ID), {
    method: "POST",
    headers: { "Content-Type": "application/json", "revision": KLAVIYO_REVISION },
    body: JSON.stringify(body)
  }).catch(function () {});
}

/* Entrants: subscribe to the Challenge Participants list on page 1 submit. */
function klaviyoSubscribeChallenge(firstName, email) {
  return klaviyoSubscribeToList(firstName, email, KLAVIYO_LIST_ID, {
    challenge_signup_at: new Date().toISOString(),
    challenge_source: "challenge.drinkhyro.com.au"
  });
}

/* Completers: subscribe to the Challenge Completions list on day-70 claim. */
function klaviyoSubscribeCompletion(firstName, email) {
  return klaviyoSubscribeToList(firstName, email, KLAVIYO_COMPLETIONS_LIST_ID, {
    challenge_completed_at: new Date().toISOString(),
    challenge_source: "challenge.drinkhyro.com.au"
  });
}

/* Tag the Shopify customer via our server-side Pages Function.
   Token stays server-side; this just posts email + first name + tag. Non-blocking.
   tag defaults to the signup tag; the tracker passes the completion tag. */
function shopifyTag(firstName, email, tag) {
  return fetch("/api/tag", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ firstName: firstName, email: email, tag: tag })
  }).then(function (r) { return r.json(); })
    .then(function (d) { return (d && d.startedAt) || null; })
    .catch(function () { return null; });
}
function shopifyTagChallenge(firstName, email) {
  return shopifyTag(firstName, email, "70-Day Challenge");
}
function shopifyTagCompleted(firstName, email) {
  return shopifyTag(firstName, email, "70-Day Challenge Completed");
}

/* Fire the "Submitted Challenge" event — this is the Klaviyo Flow trigger. */
function klaviyoTrackChallenge(firstName, email) {
  const body = {
    data: {
      type: "event",
      attributes: {
        properties: { source: "challenge.drinkhyro.com.au" },
        metric: { data: { type: "metric", attributes: { name: "Submitted Challenge" } } },
        profile: { data: { type: "profile", attributes: { email: email, first_name: firstName } } }
      }
    }
  };
  return fetch(KLAVIYO_API + "/events/?company_id=" + encodeURIComponent(KLAVIYO_COMPANY_ID), {
    method: "POST",
    headers: { "Content-Type": "application/json", "revision": KLAVIYO_REVISION },
    body: JSON.stringify(body)
  }).catch(function () {});
}

function loadMember() {
  try { return JSON.parse(localStorage.getItem(STORE_KEY)); } catch (e) { return null; }
}
function saveMember(m) { try { localStorage.setItem(STORE_KEY, JSON.stringify(m)); } catch (e) {} }

/* Local-midnight day math. Day 1 = the user's LOCAL calendar day they started.
   The clock ticks over to the next box at 00:01 local time each new day. */
function startOfLocalDay(d) {
  return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function dayFromStart(startISO, now) {
  if (!startISO) return 1;
  const start = startOfLocalDay(new Date(startISO));
  const today = startOfLocalDay(now || new Date());
  const elapsed = Math.floor((today - start) / 86400000); // whole local days since start
  return Math.min(70, Math.max(1, elapsed + 1)); // Day 1 on start day
}
/* ms until 00:01:00 local tomorrow (the tick-over moment for a new box) */
function msUntilNextLocalTick(now) {
  const n = now || new Date();
  const next = new Date(n.getFullYear(), n.getMonth(), n.getDate() + 1, 0, 1, 0, 0);
  return Math.max(1000, next - n);
}

/* ---------- Opt-in modal ---------- */
function OptinModal({ open, onClose, onSubmit, prefill }) {
  const [first, setFirst] = useS(prefill.firstName || "");
  const [email, setEmail] = useS(prefill.email || "");
  const [errs, setErrs] = useS({});

  useE(() => { if (open) { setFirst(prefill.firstName || ""); setEmail(prefill.email || ""); setErrs({}); } }, [open]);

  function submit(e) {
    e.preventDefault();
    const next = {};
    if (!first.trim()) next.first = "Pop your first name in";
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim())) next.email = "That email does not look right";
    setErrs(next);
    if (Object.keys(next).length === 0) onSubmit({ firstName: first.trim(), email: email.trim() });
  }

  return (
    <div className={"modal-back" + (open ? " show" : "")} onClick={onClose}>
      <form className="modal" onClick={e => e.stopPropagation()} onSubmit={submit}>
        <button type="button" className="close" onClick={onClose} aria-label="Close">×</button>
        <div className="lock-ic">{Icon.unlock({})}</div>
        <h3>Start your 70 days</h3>
        <p className="sub">First name and email. That is it. Your tracker is on the other side.</p>
        <div className={"field" + (errs.first ? " err" : "")}>
          <label>First name</label>
          <input value={first} onChange={e => setFirst(e.target.value)} placeholder="Alex" autoComplete="given-name" />
          {errs.first && <div className="msg">{errs.first}</div>}
        </div>
        <div className={"field" + (errs.email ? " err" : "")}>
          <label>Email</label>
          <input value={email} onChange={e => setEmail(e.target.value)} placeholder="you@email.com" type="email" autoComplete="email" />
          {errs.email && <div className="msg">{errs.email}</div>}
        </div>
        <button className="cta block big" type="submit" style={{ marginTop: 6 }}>Open my tracker</button>
        <p className="fine">Free to join. We will only email you about your challenge.</p>
      </form>
    </div>
  );
}

/* ---------- Log-in modal (existing participants, email only) ---------- */
function LoginModal({ open, onClose, onLogin }) {
  const [email, setEmail] = useS("");
  const [err, setErr] = useS("");
  const [busy, setBusy] = useS(false);

  useE(() => { if (open) { setEmail(""); setErr(""); setBusy(false); } }, [open]);

  function submit(e) {
    e.preventDefault();
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim())) { setErr("That email does not look right"); return; }
    setErr(""); setBusy(true);
    onLogin(email.trim(), function (res) {
      setBusy(false);
      if (!res.ok) setErr(res.error ? "Something went wrong, try again" : "We can't find a challenge for that email. Sign up to start.");
    });
  }

  return (
    <div className={"modal-back" + (open ? " show" : "")} onClick={onClose}>
      <form className="modal" onClick={e => e.stopPropagation()} onSubmit={submit}>
        <button type="button" className="close" onClick={onClose} aria-label="Close">×</button>
        <div className="lock-ic">{Icon.unlock({})}</div>
        <h3>Welcome back</h3>
        <p className="sub">Enter the email you signed up with and we'll take you straight to your tracker.</p>
        <div className={"field" + (err ? " err" : "")}>
          <label>Email</label>
          <input value={email} onChange={e => setEmail(e.target.value)} placeholder="you@email.com" type="email" autoComplete="email" />
          {err && <div className="msg">{err}</div>}
        </div>
        <button className="cta block big" type="submit" style={{ marginTop: 6 }} disabled={busy}>{busy ? "Finding your tracker…" : "Open my tracker"}</button>
        <p className="fine">New here? Close this and hit Start the challenge.</p>
      </form>
    </div>
  );
}

/* ---------- Vault transition overlay ---------- */
function VaultOverlay({ phase }) {
  if (!phase) return null;
  return (
    <div className={"vault-overlay " + phase}>
      <div className="vault-door left"><div className="edge"></div></div>
      <div className="vault-door right"><div className="edge"></div></div>
      <div className="vault-seam">
        <div className="ring">{Icon.unlock({})}</div>
        <div className="t">Unlocking your 70</div>
      </div>
    </div>
  );
}

/* ---------- Press bar (toggled by showPress tweak) ---------- */
function PressBar() {
  return (
    <div style={{ background: "var(--hyro-white)", borderTop: "1px solid var(--hyro-line)", borderBottom: "1px solid var(--hyro-line)", padding: "22px 0" }}>
      <div className="wrap" style={{ display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "center", gap: "16px 34px" }}>
        <span style={{ fontFamily: "var(--font-display)", textTransform: "uppercase", letterSpacing: "0.08em", fontSize: 12, color: "var(--hyro-grey-500)" }}>As seen in</span>
        {[0,1,2,3].map(i => (
          <span key={i} style={{ fontFamily: "var(--font-sans)", fontSize: 12, color: "var(--hyro-grey-300)", border: "1px dashed var(--hyro-grey-300)", borderRadius: 8, padding: "8px 18px" }}>[ Press logo ]</span>
        ))}
      </div>
    </div>
  );
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "heroCopy": "Offer-led",
  "previewDay": 39,
  "previewDayOverride": false,
  "showPress": false,
  "stickyCta": true
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [member, setMember] = useS(() => loadMember());
  const [page, setPage] = useS(() => (localStorage.getItem(PAGE_KEY) === "tracker" && loadMember()) ? "tracker" : "vault");
  const [optin, setOptin] = useS(false);
  const [login, setLogin] = useS(false); // "already started? log in" modal
  const [phase, setPhase] = useS(null); // vault transition
  const [checked, setChecked] = useS(() => (loadMember()?.checkedDays) || []);
  const [claimed, setClaimed] = useS(() => loadMember()?.claimStatus === "submitted");
  const [justChecked, setJustChecked] = useS(null);
  const [stickyShow, setStickyShow] = useS(false);
  const heroRef = useR(null);

  // Designer override: when the Preview-day slider is touched it pins the view
  // to that day. Real members run on a real clock derived from their start date.
  const previewOverride = t.previewDayOverride === true;

  // `tick` is bumped at each local-midnight rollover to force a recompute.
  const [tick, setTick] = useS(0);

  // Live current day: Day 1 on the local calendar day the member started, then
  // +1 at 00:01 local time every new day, capped at 70. Preview slider wins when
  // explicitly engaged (design tool), otherwise we always use the real clock.
  const liveDay = member ? dayFromStart(member.startDate, new Date()) : 1;
  const currentDay = previewOverride ? Math.round(t.previewDay || 1) : liveDay;

  // Roll the clock over at 00:01 local time each new calendar day. Reschedules
  // itself so it keeps ticking one box per day for the life of the session.
  useE(() => {
    if (page !== "tracker" || !member || previewOverride) return;
    let timer;
    const schedule = () => {
      timer = setTimeout(() => {
        setTick(x => x + 1); // recompute currentDay from the new local date
        schedule();
      }, msUntilNextLocalTick(new Date()));
    };
    schedule();
    return () => clearTimeout(timer);
  }, [page, member, previewOverride]);

  // Also recompute if the tab was backgrounded across a midnight (timers can be
  // throttled/suspended), so the day is correct the moment the user returns.
  useE(() => {
    if (previewOverride) return;
    const onVis = () => { if (!document.hidden) setTick(x => x + 1); };
    document.addEventListener("visibilitychange", onVis);
    return () => document.removeEventListener("visibilitychange", onVis);
  }, [previewOverride]);

  // On load, re-verify the member's start date against the server (source of
  // truth). Adopts the server date if localStorage was cleared/tampered or the
  // user is on a new device. Runs once per email.
  useE(() => {
    if (!member || !member.email) return;
    let cancelled = false;
    fetch("/api/status?email=" + encodeURIComponent(member.email))
      .then(function (r) { return r.json(); })
      .then(function (d) {
        if (cancelled || !d || !d.ok || !d.found || !d.startedAt) return;
        if (member.startDate !== d.startedAt) {
          const m = loadMember();
          if (m && (m.email || "").toLowerCase() === member.email.toLowerCase()) {
            m.startDate = d.startedAt; saveMember(m); setMember({ ...m }); setTick(x => x + 1);
          }
        }
      })
      .catch(function () {});
    return function () { cancelled = true; };
  }, [member && member.email]);

  useE(() => { localStorage.setItem(PAGE_KEY, page); }, [page]);

  // Deep link from email: /?email=<addr> (or ?e=<addr>) takes a known
  // participant straight to their tracker. Falls back to the page if the email
  // hasn't started. Runs once on mount; strips the param from the URL after.
  useE(() => {
    try {
      const params = new URLSearchParams(window.location.search);
      const qEmail = (params.get("email") || params.get("e") || "").trim();
      if (!qEmail || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(qEmail)) return;
      loginWithEmail(qEmail, function (res) {
        // If not found, leave them on page 1 to sign up (no error popup).
        // Clean the email out of the visible URL either way.
        try {
          const clean = window.location.origin + window.location.pathname;
          window.history.replaceState({}, document.title, clean);
        } catch (e) {}
      });
    } catch (e) {}
  }, []);

  // sticky CTA visibility (mobile) — show once hero is scrolled past
  useE(() => {
    if (page !== "vault") { setStickyShow(false); return; }
    const el = heroRef.current;
    if (!el || !("IntersectionObserver" in window)) return;
    const io = new IntersectionObserver(([e]) => setStickyShow(!e.isIntersecting), { threshold: 0 });
    io.observe(el);
    return () => io.disconnect();
  }, [page]);

  function openCta() { setOptin(true); }

  function handleSubmit({ firstName, email }) {
    setOptin(false);
    // Always fire Klaviyo subscribe + flow event and Shopify entry tag (the tag
    // call stamps the server start date once and never resets it).
    try { klaviyoSubscribeChallenge(firstName, email); klaviyoTrackChallenge(firstName, email); } catch (e) {}
    try { shopifyTagChallenge(firstName, email); } catch (e) {}

    // Check the server for an existing challenge start date. If this email has
    // already started (e.g. they hit the big CTA instead of "Jump back in"),
    // resume their real progress at the correct day with prior days pre-ticked.
    fetch("/api/status?email=" + encodeURIComponent(email))
      .then(function (r) { return r.json(); })
      .then(function (d) {
        if (d && d.ok && d.found && d.startedAt) {
          startMember(firstName, email, d.startedAt, d.day, d.completed);
        } else {
          startMember(firstName, email, new Date().toISOString(), 1, false);
        }
      })
      .catch(function () {
        startMember(firstName, email, new Date().toISOString(), 1, false);
      });

    runTransition();
  }

  // Build/refresh the member from a (server) start date and open the tracker.
  // Pre-ticks every day BEFORE today so a resuming user sees their streak.
  function startMember(firstName, email, startISO, serverDay, completed) {
    const existing = loadMember();
    const today = Math.max(1, Math.min(70, serverDay || dayFromStart(startISO, new Date())));
    const prior = [];
    for (let i = 1; i < today; i++) prior.push(i);
    const sameUser = existing && (existing.email || "").toLowerCase() === (email || "").toLowerCase();
    const base = sameUser ? existing : {
      checkedDays: [], subscriptionStatus: "active", claimStatus: "none",
      memberId: "mbr_" + Math.random().toString(36).slice(2, 9)
    };
    const checkedUnion = [...new Set([...(base.checkedDays || []), ...prior])].sort(function (a, b) { return a - b; });
    const m = Object.assign({}, base, {
      firstName: firstName || base.firstName || "",
      email: email,
      startDate: startISO,
      checkedDays: checkedUnion,
      claimStatus: completed ? "submitted" : (base.claimStatus || "none")
    });
    saveMember(m);
    setMember(m);
    setChecked(m.checkedDays || []);
    setClaimed(m.claimStatus === "submitted");
    setTick(x => x + 1);
  }

  // "Already started? Log in" — email only. Pulls the server start date and
  // opens the tracker at the correct day. No restart, any device.
  function loginWithEmail(email, onResult) {
    const em = (email || "").trim();
    fetch("/api/status?email=" + encodeURIComponent(em))
      .then(function (r) { return r.json(); })
      .then(function (d) {
        if (!d || !d.ok || !d.found || !d.startedAt) {
          onResult({ ok: false });
          return;
        }
        const existing = loadMember();
        const fn = (existing && existing.firstName) || "";
        startMember(fn, em, d.startedAt, d.day, d.completed);
        setLogin(false);
        onResult({ ok: true });
        runTransition();
      })
      .catch(function () { onResult({ ok: false, error: true }); });
  }

  function runTransition() {
    setPhase("closing");
    setTimeout(() => setPhase("closed"), 820);
    setTimeout(() => { setPage("tracker"); window.scrollTo(0, 0); }, 1320);
    setTimeout(() => setPhase("opening"), 1380);
    setTimeout(() => setPhase(null), 2200);
  }

  function toggleDay(day) {
    if (day > currentDay) return;
    setChecked(prev => {
      const has = prev.includes(day);
      const next = has ? prev.filter(d => d !== day) : [...prev, day].sort((a, b) => a - b);
      const m = loadMember(); if (m) { m.checkedDays = next; saveMember(m); }
      if (!has) { setJustChecked(day); setTimeout(() => setJustChecked(null), 360); }
      return next;
    });
  }

  function claim(claimName, claimEmail) {
    const m = loadMember(); if (m) { m.claimStatus = "submitted"; saveMember(m); }
    setClaimed(true);
    // Tag the Shopify customer as having completed the 70-day challenge.
    const fn = (claimName || (m && m.firstName) || "").trim();
    const em = (claimEmail || (m && m.email) || "").trim();
    if (em) {
      try { shopifyTagCompleted(fn, em); } catch (e) {}
      try { klaviyoSubscribeCompletion(fn, em); } catch (e) {}
    }
  }

  function retake() {
    const m = loadMember();
    if (m) { m.checkedDays = []; m.claimStatus = "none"; m.startDate = new Date().toISOString(); saveMember(m); }
    setChecked([]); setClaimed(false); setTweak("previewDayOverride", false);
    window.scrollTo(0, 0);
  }

  function resetProgress() {
    localStorage.removeItem(STORE_KEY);
    localStorage.removeItem(PAGE_KEY);
    setMember(null); setChecked([]); setClaimed(false); setPage("vault");
    window.scrollTo(0, 0);
  }

  return (
    <>
      {page === "vault" && (
        <>
          <VaultDoor onCta={openCta} onLogin={() => setLogin(true)} t={t} heroRef={heroRef} />
          {t.showPress && (
            <div style={{ position: "relative", marginTop: -1 }}><PressBar /></div>
          )}
          {/* sticky bottom CTA bar removed on mobile per Nathan 2026-06-03 */}
        </>
      )}

      {page === "tracker" && member && (
        <Tracker
          member={member} currentDay={currentDay} checked={checked}
          onToggle={toggleDay} claimed={claimed} onClaim={claim} onRetake={retake} justChecked={justChecked}
        />
      )}
      {page === "tracker" && !member && (
        /* safety: no member but on tracker — send back */
        <div style={{ padding: 60, textAlign: "center" }}>
          <button className="cta" onClick={() => setPage("vault")}>Back to the challenge</button>
        </div>
      )}

      <OptinModal open={optin} onClose={() => setOptin(false)} onSubmit={handleSubmit} prefill={member || {}} />
      <LoginModal open={login} onClose={() => setLogin(false)} onLogin={loginWithEmail} />
      <VaultOverlay phase={phase} />

      {/* TWEAKS */}
      <TweaksPanel>
        <TweakSection label="Page 1 — Hero" />
        <TweakSelect label="Hero headline" value={t.heroCopy}
          options={["Default", "Problem-led", "Offer-led", "Returning subscriber"]}
          onChange={v => setTweak("heroCopy", v)} />
        <TweakToggle label="Show ‘as seen in’ bar" value={t.showPress} onChange={v => setTweak("showPress", v)} />
        <TweakToggle label="Sticky mobile CTA" value={t.stickyCta} onChange={v => setTweak("stickyCta", v)} />

        <TweakSection label="Page 2 — Tracker preview" />
        <TweakToggle label="Override clock (preview day)" value={t.previewDayOverride} onChange={v => setTweak("previewDayOverride", v)} />
        <TweakSlider label="Preview day" value={t.previewDay} min={1} max={70} step={1} unit=" / 70"
          onChange={v => { setTweak("previewDay", v); setTweak("previewDayOverride", true); }} />
        <TweakButton label="View the tracker" onClick={() => { if (!member) { handleSubmit({ firstName: "Alex", email: "alex@email.com" }); } else { setPage("tracker"); window.scrollTo(0,0); } }} />
        <TweakButton label="Reset progress · back to start" onClick={resetProgress} />
      </TweaksPanel>
    </>
  );
}

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