/* global React, ReactDOM, LogoMark, useAppTweaks, AppTweaks, JSZip */
const { useState, useRef, useCallback, useEffect } = React;

const PRICE_PER_PHOTO = 7;
const MAX_PHOTOS = 50; // Mirror MAX_PHOTOS_PER_SESSION on the Worker

// Pathname routing — same bundle serves /order (compose) and /order/confirmation
// (post-payment delivery). Substring match handles trailing slashes and *.html
// fallbacks served by Cloudflare Pages.
const isConfirmationRoute = () => /\/order\/confirmation/.test(window.location.pathname);

function uid() {
  return Math.random().toString(36).slice(2, 9);
}

function formatBytes(b) {
  if (b < 1024) return `${b} B`;
  if (b < 1024 * 1024) return `${(b / 1024).toFixed(0)} KB`;
  return `${(b / 1024 / 1024).toFixed(1)} MB`;
}

function formatEta(seconds) {
  if (seconds <= 0) return "almost there";
  if (seconds < 60) return `${seconds} sec`;
  const m = Math.floor(seconds / 60);
  const s = seconds % 60;
  if (m < 60) return s ? `${m} min ${s} sec` : `${m} min`;
  return `${Math.floor(m / 60)} hr ${m % 60} min`;
}

function getStoredRef() {
  try {
    const v = JSON.parse(localStorage.getItem("pfx_ref") || "null");
    if (v && v.exp > Date.now()) return v.ref;
  } catch (_) {}
  return null;
}

function getQueryParam(name) {
  return new URLSearchParams(location.search).get(name);
}

// ============================================================
// Top-level router
// ============================================================
function App() {
  const [tweaks, setTweak] = useAppTweaks();
  if (isConfirmationRoute()) {
    return (
      <>
        <OrderNav />
        <main className="batch-page">
          <ConfirmationView />
          <AppTweaks tweaks={tweaks} setTweak={setTweak} pageContext="sub" />
        </main>
      </>
    );
  }
  return (
    <>
      <OrderNav />
      <ComposeView />
      <AppTweaks tweaks={tweaks} setTweak={setTweak} pageContext="sub" />
    </>
  );
}

// ============================================================
// /order — compose: pick photos, upload as you go, pay
// ============================================================
function ComposeView() {
  // photo: { id, file, name, size, url, index, status: "queued" | "uploading" | "uploaded" | "failed", err? }
  const [photos, setPhotos] = useState([]);
  const [drag, setDrag] = useState(false);
  const [sessionId, setSessionId] = useState(null);
  const [initError, setInitError] = useState(null);
  const [paying, setPaying] = useState(false);
  const inputRef = useRef(null);
  const nextIndexRef = useRef(0);
  const sessionInitInflightRef = useRef(null);
  const cancelled = getQueryParam("cancelled") === "1";

  const total = photos.length * PRICE_PER_PHOTO;
  const eta = Math.max(22, photos.length * 22);
  const allUploaded = photos.length > 0 && photos.every((p) => p.status === "uploaded");
  const anyUploading = photos.some((p) => p.status === "queued" || p.status === "uploading");

  // Lazy-init session on first photo selection. Idempotent: in-flight call is
  // shared across rapid drops so we don't accidentally create two sessions.
  const ensureSession = useCallback(async () => {
    if (sessionId) return sessionId;
    if (sessionInitInflightRef.current) return sessionInitInflightRef.current;
    const promise = (async () => {
      const res = await fetch("/api/orders/init", { method: "POST" });
      if (!res.ok) throw new Error(`init failed: HTTP ${res.status}`);
      const body = await res.json();
      setSessionId(body.sessionId);
      return body.sessionId;
    })();
    sessionInitInflightRef.current = promise;
    try {
      return await promise;
    } finally {
      sessionInitInflightRef.current = null;
    }
  }, [sessionId]);

  const uploadOne = useCallback(async (sid, photo) => {
    setPhotos((ps) => ps.map((p) => (p.id === photo.id ? { ...p, status: "uploading" } : p)));
    try {
      const fd = new FormData();
      fd.append("image", photo.file);
      const res = await fetch(`/api/upload?session=${encodeURIComponent(sid)}&i=${photo.index}`, {
        method: "POST",
        body: fd,
      });
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw new Error(body.error || `HTTP ${res.status}`);
      }
      setPhotos((ps) => ps.map((p) => (p.id === photo.id ? { ...p, status: "uploaded" } : p)));
    } catch (err) {
      console.warn(`upload ${photo.index} failed:`, err);
      setPhotos((ps) =>
        ps.map((p) => (p.id === photo.id ? { ...p, status: "failed", err: err.message || String(err) } : p)),
      );
    }
  }, []);

  const [capWarning, setCapWarning] = useState(null);

  const addFiles = useCallback(
    async (files) => {
      const incoming = [...files].filter((f) => f.type.startsWith("image/"));
      if (incoming.length === 0) return;

      // Enforce the per-session cap on the client so the user gets a clear
      // message before we waste an upload that the Worker would reject.
      const room = MAX_PHOTOS - photos.length;
      let toAdd = incoming;
      if (incoming.length > room) {
        toAdd = incoming.slice(0, Math.max(0, room));
        setCapWarning(
          `${MAX_PHOTOS}-photo limit per order. Added the first ${toAdd.length} of ${incoming.length}.` +
            (toAdd.length === 0 ? " Remove some to add more." : ""),
        );
      } else {
        setCapWarning(null);
      }
      if (toAdd.length === 0) return;

      let sid;
      try {
        sid = await ensureSession();
      } catch (err) {
        setInitError(err.message || String(err));
        return;
      }

      const queued = toAdd.map((f) => ({
        id: uid(),
        file: f,
        name: f.name,
        size: f.size,
        url: URL.createObjectURL(f),
        index: nextIndexRef.current++,
        status: "queued",
      }));
      setPhotos((p) => [...p, ...queued]);
      // Fire uploads in parallel.
      queued.forEach((q) => { uploadOne(sid, q); });
    },
    [ensureSession, uploadOne, photos.length],
  );

  const onPick = (e) => addFiles(e.target.files || []);
  const onDrop = (e) => {
    e.preventDefault();
    setDrag(false);
    addFiles(e.dataTransfer.files || []);
  };

  const remove = (id) => {
    // Local-only — the R2 object will expire via lifecycle TTL since we won't
    // pay for it. Kie kickoff in the webhook iterates session.photos, so a
    // photo removed locally still has its R2 object but the session won't
    // include it after the next /api/upload to a different index.
    setPhotos((p) => p.filter((x) => x.id !== id));
  };

  const retryUpload = (photo) => {
    if (!sessionId) return;
    uploadOne(sessionId, photo);
  };

  const startCheckout = async () => {
    if (!sessionId || photos.length === 0 || !allUploaded || paying) return;
    setPaying(true);
    try {
      const res = await fetch("/api/create-checkout", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ sessionId, ref: getStoredRef() || undefined }),
      });
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw new Error(body.error || `HTTP ${res.status}`);
      }
      const { url } = await res.json();
      // Same-tab redirect — the user will return to /order/confirmation
      // after Stripe. We persist nothing in JS state because R2 holds the
      // photos and the session lives server-side in KV.
      window.location.href = url;
    } catch (err) {
      setPaying(false);
      alert(`Couldn't start checkout: ${err.message}`);
    }
  };

  return (
    <main className="batch-page">
      <div className="batch-head-grid">
        <header className="batch-head">
          <div className="eyebrow">Order</div>
          <h1 className="h1 batch-h">Professional, MLS-ready photos.</h1>
          <p className="batch-sub">
            Every photo comes back sized and formatted for the platforms that actually publish your listing.
          </p>
          <div className="batch-spec-row">
            <span className="batch-spec-chip"><b>2K</b> resolution</span>
            <span className="batch-spec-chip"><b>4:3</b> aspect</span>
            <span className="batch-spec-chip"><b>JPG</b> · sRGB</span>
            <span className="batch-spec-chip">under <b>10&nbsp;MB</b></span>
          </div>
          <p className="batch-sub" style={{ marginTop: 14 }}>
            Drag and drop, or <b>import directly from your phone</b>.
          </p>
        </header>
        <PlatformList />
      </div>

      {cancelled && (
        <div className="batch-orphan">
          <b>Payment cancelled.</b> Your photos are still uploaded — click Pay again whenever you're ready.
        </div>
      )}

      {initError && (
        <div className="batch-orphan">
          <b>Couldn't start your order:</b> {initError}. Refresh the page and try again.
        </div>
      )}

      {capWarning && (
        <div className="batch-orphan">
          {capWarning}
        </div>
      )}


      <div className="batch-grid">
        <section className="batch-stage">
          <DropZone
            hasPhotos={photos.length > 0}
            drag={drag}
            setDrag={setDrag}
            onDrop={onDrop}
            inputRef={inputRef}
            onPick={onPick}
          />
          {photos.length > 0 && (
            <div className="batch-list-head">
              <div className="batch-list-count">
                <b>{photos.length}</b> {photos.length === 1 ? "photo" : "photos"}
                {anyUploading && " · uploading…"}
                {!anyUploading && allUploaded && " · ready to pay"}
              </div>
              <div className="batch-list-actions">
                <button className="btn btn-ghost btn-sm" onClick={() => inputRef.current?.click()}>+ Add more</button>
              </div>
            </div>
          )}
          {photos.length > 0 && (
            <ul className="batch-list">
              {photos.map((p, i) => (
                <li key={p.id} className={`batch-item ${p.status === "failed" ? "is-failed" : ""}`}>
                  <div className="batch-item-thumb">
                    <img src={p.url} alt="" />
                    <span className="batch-item-n">{String(i + 1).padStart(2, "0")}</span>
                  </div>
                  <div className="batch-item-meta">
                    <div className="batch-item-name" title={p.name}>{p.name}</div>
                    <div className="batch-item-size">
                      {formatBytes(p.size)} · ${PRICE_PER_PHOTO}
                      {p.status === "queued" && " · queued"}
                      {p.status === "uploading" && " · uploading…"}
                      {p.status === "uploaded" && " · ✓ ready"}
                      {p.status === "failed" && (
                        <>
                          {" · ✗ "}
                          <a href="#" onClick={(e) => { e.preventDefault(); retryUpload(p); }}>retry</a>
                        </>
                      )}
                    </div>
                  </div>
                  <button className="batch-item-x" onClick={() => remove(p.id)} aria-label={`Remove ${p.name}`}>✕</button>
                </li>
              ))}
            </ul>
          )}
        </section>

        <aside className="batch-cart">
          <div className="cart-card">
            <div className="cart-row cart-row-head">
              <span className="eyebrow" style={{ margin: 0 }}>Order</span>
            </div>
            <div className="cart-line">
              <span>Photos to fix</span>
              <span className="cart-line-v"><b>{photos.length}</b></span>
            </div>
            <div className="cart-line">
              <span>Per photo</span>
              <span className="cart-line-v">${PRICE_PER_PHOTO}.00</span>
            </div>
            <div className="cart-line cart-line-total">
              <span>Total</span>
              <span className="cart-line-v cart-total">
                <span className="cart-total-n">${total.toFixed(2)}</span>
              </span>
            </div>
            <div className="cart-eta">
              <span className="cart-eta-dot" />
              <span>
                {photos.length === 0
                  ? "Drop photos to see your total"
                  : `Delivered in ~${formatEta(eta)}, no editor queue.`}
              </span>
            </div>
            <button
              className="btn btn-primary btn-lg cart-cta"
              disabled={photos.length === 0 || !allUploaded || paying}
              onClick={startCheckout}
            >
              {photos.length === 0
                ? "Add photos to continue"
                : paying
                ? "Sending you to Stripe…"
                : !allUploaded
                ? "Uploading… please wait"
                : `Pay $${total.toFixed(2)} · process now`}
            </button>
            <div className="cart-fine">
              <div className="cart-fine-head">How you'll get them</div>
              <div>✓ Download a ZIP with every photo, MLS-sized, no watermark</div>
              <div>✓ Originals never stored · enhanced versions auto-delete after 14 days</div>
            </div>
          </div>

          <a href="/" className="batch-back">← Back to home</a>
        </aside>
      </div>
    </main>
  );
}

// ============================================================
// /order/confirmation — post-payment: poll status, build ZIP
// ============================================================
function ConfirmationView() {
  const sessionId = getQueryParam("session");
  const [statusBody, setStatusBody] = useState(null);
  const [pollErr, setPollErr] = useState(null);

  useEffect(() => {
    if (!sessionId) return;
    let cancelled = false;
    const tick = async () => {
      while (!cancelled) {
        try {
          const res = await fetch(`/api/status?session=${encodeURIComponent(sessionId)}`);
          if (res.ok) {
            const body = await res.json();
            if (cancelled) return;
            setStatusBody(body);
            setPollErr(null);
            if (body.allDone) return;
          } else if (res.status === 404) {
            setPollErr("session not found");
            return;
          }
        } catch (err) {
          setPollErr(err.message || String(err));
        }
        await new Promise((r) => setTimeout(r, 2500));
      }
    };
    tick();
    return () => { cancelled = true; };
  }, [sessionId]);

  if (!sessionId) {
    return (
      <section className="batch-delivery">
        <header className="batch-head">
          <div className="eyebrow">Order</div>
          <h1 className="h1 batch-h">Missing session.</h1>
          <p className="batch-sub">
            We couldn't find an order in this URL. <a href="/order">Start a new order →</a>
          </p>
        </header>
      </section>
    );
  }

  // Initial load before first /api/status response
  if (!statusBody && !pollErr) {
    return (
      <section className="batch-delivery">
        <header className="batch-head">
          <div className="eyebrow">Confirming payment</div>
          <h1 className="h1 batch-h">Just a moment…</h1>
          <p className="batch-sub">Confirming your order.</p>
        </header>
      </section>
    );
  }

  if (pollErr === "session not found") {
    return (
      <section className="batch-delivery">
        <header className="batch-head">
          <div className="eyebrow">Order</div>
          <h1 className="h1 batch-h">Order not found.</h1>
          <p className="batch-sub">
            Your session isn't on file. If you just paid, refresh in a few seconds.
            Otherwise <a href="/order">start a new order →</a>.
          </p>
        </header>
      </section>
    );
  }

  return <DeliveryView sessionId={sessionId} statusBody={statusBody} />;
}

function DeliveryView({ sessionId, statusBody }) {
  const [zipping, setZipping] = useState(false);
  const [zipUrl, setZipUrl] = useState(null);
  const [error, setError] = useState(null);

  const photos = statusBody?.photos || {};
  const indices = Object.keys(photos)
    .map((k) => Number(k))
    .filter((n) => Number.isInteger(n))
    .sort((a, b) => a - b);
  const total = indices.length;
  const doneCount = indices.filter((i) => photos[String(i)].status === "done").length;
  const failedCount = indices.filter((i) => photos[String(i)].status === "failed").length;
  const allDone = !!statusBody?.allDone;
  const pct = total === 0 ? 0 : Math.round((doneCount / total) * 100);
  const remaining = Math.max(0, total - doneCount - failedCount);

  const buildZip = async () => {
    if (!window.JSZip) {
      setError("ZIP library failed to load — try refreshing the page.");
      return;
    }
    setZipping(true);
    setError(null);
    try {
      const zip = new window.JSZip();
      for (const i of indices) {
        if (photos[String(i)].status !== "done") continue;
        const res = await fetch(`/api/photo?session=${encodeURIComponent(sessionId)}&i=${i}`);
        if (!res.ok) throw new Error(`couldn't fetch photo ${i + 1}`);
        const blob = await res.blob();
        zip.file(`pixfixco-${String(i + 1).padStart(2, "0")}.jpg`, blob);
      }
      const blob = await zip.generateAsync({ type: "blob" });
      setZipUrl(URL.createObjectURL(blob));
    } catch (err) {
      setError(err.message || String(err));
    } finally {
      setZipping(false);
    }
  };

  return (
    <section className="batch-delivery">
      <header className="batch-head">
        <div className="eyebrow">{allDone ? "Ready to download" : "Enhancing your photos"}</div>
        <h1 className="h1 batch-h">
          {allDone
            ? `${total} ${total === 1 ? "photo" : "photos"} ready.`
            : `Working on your photos${remaining > 0 ? ` — ~${formatEta(remaining * 22)} to go` : "…"}`}
        </h1>
        <p className="batch-sub">
          {allDone
            ? "Click below to download a ZIP with every enhanced photo, MLS-sized, no watermark."
            : "Keep this tab open. We'll have a download ready the moment all photos finish."}
        </p>
      </header>

      <div className="dv-progress">
        <div className="dv-bar"><div className="dv-bar-fill" style={{ width: `${pct}%` }} /></div>
        <div className="dv-bar-label">
          <span>{doneCount} / {total} ready{failedCount > 0 ? ` · ${failedCount} failed` : ""}</span>
          <span>{pct}%</span>
        </div>
      </div>

      <ul className="dv-grid">
        {indices.map((i) => {
          const p = photos[String(i)];
          const cls = p.status === "done" ? "is-done" : p.status === "failed" ? "is-failed" : "is-working";
          return (
            <li key={i} className={`dv-cell ${cls}`}>
              {p.status === "done" && p.url ? <img src={p.url} alt="" /> : <div className="dv-cell-placeholder" />}
              {p.status === "done" && (
                <span className="dv-cell-badge" aria-label="Done">
                  <svg width="12" height="12" viewBox="0 0 16 16" fill="none">
                    <path d="M3 8.5L6.5 12L13 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
                  </svg>
                </span>
              )}
              {p.status !== "done" && p.status !== "failed" && <span className="dv-cell-spinner" />}
            </li>
          );
        })}
      </ul>

      {error && <div className="batch-orphan" style={{ marginTop: 16 }}>{error}</div>}

      <div className="dv-actions">
        {!zipUrl ? (
          <button
            className="btn btn-primary btn-lg dv-download"
            disabled={!allDone || zipping}
            onClick={buildZip}
          >
            {zipping
              ? "Building ZIP…"
              : allDone
              ? `↓ Download ZIP · ${total} ${total === 1 ? "photo" : "photos"}`
              : "Preparing your ZIP…"}
          </button>
        ) : (
          <a className="btn btn-primary btn-lg dv-download" href={zipUrl} download="pixfixco-photos.zip">
            ↓ Download ZIP · {total} {total === 1 ? "photo" : "photos"}
          </a>
        )}
        {allDone && (
          <p className="dv-fine">
            ZIP includes every enhanced photo as JPG. Originals are not retained — these auto-delete after 14 days.
          </p>
        )}
      </div>
    </section>
  );
}

// ============================================================
// Shared chrome
// ============================================================
const PLATFORM_CHECK_PATH = "M3.5 8.5L6.5 11.5L12.5 5";

function PlatformList() {
  const ref = useRef(null);
  const [revealed, setRevealed] = useState(false);
  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      setRevealed(true);
      return;
    }
    const el = ref.current;
    if (!el) return;
    const obs = new IntersectionObserver((entries) => {
      entries.forEach((e) => { if (e.isIntersecting) setRevealed(true); });
    }, { threshold: 0.3 });
    obs.observe(el);
    return () => obs.disconnect();
  }, []);
  const platforms = ["MLS", "Zillow", "Redfin", "Realtor.com"];
  return (
    <div ref={ref} className={`pf-list ${revealed ? "is-revealed" : ""}`} aria-label="Ready for these platforms">
      <div className="pf-list-eyebrow">Ready for</div>
      <ul className="pf-list-items">
        {platforms.map((name) => (
          <li key={name} className="pf-list-item">
            <span className="pf-list-check"><svg viewBox="0 0 16 16"><path d={PLATFORM_CHECK_PATH} /></svg></span>
            <span className="pf-list-name">{name}</span>
          </li>
        ))}
        <li className="pf-list-item is-more">
          <span className="pf-list-check"><svg viewBox="0 0 16 16"><path d={PLATFORM_CHECK_PATH} /></svg></span>
          <span className="pf-list-name">And more</span>
        </li>
      </ul>
    </div>
  );
}

function OrderNav() {
  return (
    <nav className="nav">
      <div className="nav-inner">
        <a href="/" style={{ textDecoration: "none", color: "inherit" }}>
          <LogoMark />
        </a>
        <div className="nav-links">
          <a href="/gallery.html">Gallery</a>
          <a href="/pricing.html">Pricing</a>
          <a href="/how-it-works.html">How it works</a>
          <a href="/photo-guide.html">Photo guide</a>
        </div>
        <div className="nav-cta">
          <a className="btn btn-ghost" href="/">Single photo</a>
        </div>
      </div>
    </nav>
  );
}

function DropZone({ hasPhotos, drag, setDrag, onDrop, inputRef, onPick }) {
  return (
    <label
      className={`batch-drop ${drag ? "is-drag" : ""} ${hasPhotos ? "is-compact" : ""}`}
      onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
      onDragLeave={() => setDrag(false)}
      onDrop={onDrop}
    >
      <input ref={inputRef} type="file" accept="image/*" multiple hidden onChange={onPick} />
      <div className="batch-drop-inner">
        <div className="batch-drop-icon" aria-hidden="true">
          <svg width="40" height="40" viewBox="0 0 24 24" fill="none">
            <path d="M12 16V4M12 4L7 9M12 4L17 9" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
            <path d="M4 16V18C4 19.1046 4.89543 20 6 20H18C19.1046 20 20 19.1046 20 18V16" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
          </svg>
        </div>
        <div className="batch-drop-title">
          {hasPhotos ? "Drop more photos" : "Drop your listing photos here"}
        </div>
        <div className="batch-drop-sub">
          or <span className="link-inline">browse</span> — JPG or PNG, up to 25 MB each. {MAX_PHOTOS}-photo max per order.
        </div>
        {!hasPhotos && (
          <div className="batch-drop-chips">
            <span className="chip">$7 per photo</span>
            <span className="chip">~22 sec each</span>
            <span className="chip">No subscription</span>
          </div>
        )}
      </div>
    </label>
  );
}

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