/* Shared primitives used by all three direction skins.
   Exported to window so the per-direction script files can use them.
*/

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

// ============================================================
// haptic — tiny wrapper around navigator.vibrate
// ============================================================
function haptic(ms = 8) {
  try { if (navigator.vibrate) navigator.vibrate(ms); } catch (e) {}
}

// ============================================================
// txField — resolve a field that may be either a bilingual { en, no }
// object (placeholder data) or a plain string (WooCommerce, already localized
// by Polylang/WPML or the store base language). Always returns a string.
// ============================================================
function txField(v, lang = "en") {
  if (v == null) return "";
  if (typeof v === "object" && !Array.isArray(v)) {
    return v[lang] ?? v.en ?? Object.values(v)[0] ?? "";
  }
  return String(v);
}

// ============================================================
// useTick — re-renders every `ms` for live displays (countdown, cursor coords)
// ============================================================
function useTick(ms = 1000) {
  const [, setN] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setN(n => n + 1), ms);
    return () => clearInterval(t);
  }, [ms]);
}

// ============================================================
// Countdown — formatted DD : HH : MM : SS to a fixed ISO target.
// Renders only the parts (no chrome) — each skin styles them.
// ============================================================
function useCountdown(iso) {
  useTick(1000);
  const target = useMemo(() => new Date(iso).getTime(), [iso]);
  const now = Date.now();
  let diff = Math.max(0, target - now);
  const d  = Math.floor(diff / 86_400_000); diff -= d * 86_400_000;
  const h  = Math.floor(diff / 3_600_000);  diff -= h * 3_600_000;
  const m  = Math.floor(diff / 60_000);     diff -= m * 60_000;
  const s  = Math.floor(diff / 1000);
  const pad = (n, w = 2) => String(n).padStart(w, "0");
  return { d: pad(d, 2), h: pad(h), m: pad(m), s: pad(s), done: target - Date.now() <= 0 };
}

// ============================================================
// Redacted — censored span. Reveal-on-hover by default; if revealAll is true,
// shows the underlying word with a subtle highlight (for "show all decrypted" tweak).
// ============================================================
function Redacted({ children, revealAll, intensity = 0.8, accent = "#c8493a" }) {
  const [hover, setHover] = useState(false);
  const revealed = revealAll || hover;
  return (
    <span
      className="redacted"
      onMouseEnter={() => { setHover(true); haptic(6); }}
      onMouseLeave={() => setHover(false)}
      style={{
        position: "relative",
        display: "inline-block",
        cursor: "help",
        padding: "0 0.18em",
        margin: "0 0.04em",
        transition: "background 160ms ease",
        // Outer keeps inherited color so currentColor resolves to the page's
        // foreground (dark on paper, cream on dark) — that's the bar fill.
        background: revealed
          ? `color-mix(in oklch, ${accent} ${intensity * 22}%, transparent)`
          : "currentColor",
        userSelect: "none",
        whiteSpace: "nowrap",
        verticalAlign: "baseline",
      }}
    >
      {/* Inner span flips text visibility independently of the bar fill. */}
      <span style={{
        color: revealed ? "inherit" : "transparent",
        transition: "color 160ms ease",
      }}>{children}</span>
    </span>
  );
}

// Render an array of tokens (strings | { r: "word" }) into a flow of text.
function RedactedRich({ tokens, revealAll, accent }) {
  return (
    <span>
      {tokens.map((t, i) => {
        if (typeof t === "string") return <span key={i}>{t}</span>;
        return <Redacted key={i} revealAll={revealAll} accent={accent}>{t.r}</Redacted>;
      })}
    </span>
  );
}

// ============================================================
// CustomCursor — modes: "off" | "minimal" | "tracking"
//   minimal: thin ring + center dot, that's it
//   tracking: ring + dot + faint hairlines + tiny coord chip
// Hidden on touch/coarse pointers automatically.
// ============================================================
function CustomCursor({ mode = "minimal", accent = "#c8493a" }) {
  const [pos, setPos] = useState({ x: -100, y: -100, vis: false });
  const [overInteractive, setOver] = useState(false);
  const enabled = mode !== "off";

  useEffect(() => {
    if (!enabled) return;
    const move = (e) => {
      setPos({ x: e.clientX, y: e.clientY, vis: true });
      const el = e.target;
      const tag = (el?.tagName || "").toLowerCase();
      setOver(["a", "button"].includes(tag) || el?.dataset?.interactive === "true" || !!el?.closest?.("[data-interactive='true']"));
    };
    const leave = () => setPos(p => ({ ...p, vis: false }));
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseleave", leave);
    document.body.style.cursor = "none";
    return () => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseleave", leave);
      document.body.style.cursor = "";
    };
  }, [enabled]);

  const coarse = useMemo(() => window.matchMedia?.("(pointer: coarse)")?.matches, []);
  if (!enabled || coarse) return null;

  const ringR = overInteractive ? 14 : 7;
  const stroke = overInteractive ? accent : "rgba(236,229,216,0.7)";

  return (
    <div style={{
      position: "fixed", inset: 0, pointerEvents: "none", zIndex: 9999,
      opacity: pos.vis ? 1 : 0, transition: "opacity 120ms",
      fontFamily: "'JetBrains Mono', monospace",
    }}>
      {mode === "tracking" && (
        <>
          <div style={{ position: "fixed", left: pos.x, top: 0, bottom: 0, width: 1, background: "rgba(236,229,216,0.035)" }} />
          <div style={{ position: "fixed", top: pos.y, left: 0, right: 0, height: 1, background: "rgba(236,229,216,0.035)" }} />
        </>
      )}
      <svg width="32" height="32" viewBox="0 0 32 32"
           style={{ position: "fixed", left: pos.x - 16, top: pos.y - 16 }}>
        <circle cx="16" cy="16" r={ringR} fill="none" stroke={stroke} strokeWidth="0.7"
                style={{ transition: "r 160ms ease" }} />
        <circle cx="16" cy="16" r="1.1" fill={overInteractive ? accent : "rgba(236,229,216,0.95)"} />
      </svg>
      {mode === "tracking" && overInteractive && (
        <div style={{
          position: "fixed", left: pos.x + 18, top: pos.y + 14,
          fontSize: 9, letterSpacing: "0.18em", color: accent,
          padding: "2px 6px", border: `1px solid ${accent}`,
          textTransform: "uppercase", whiteSpace: "nowrap",
        }}>
          ● LOCK
        </div>
      )}
    </div>
  );
}

// ============================================================
// Lang toggle pill (EN / NO)
// ============================================================
function LangToggle({ lang, setLang, accent }) {
  const opts = ["en", "no", "es"];
  const labelFor = c => (c === "no" ? "NB" : c.toUpperCase());
  // Highlight the truly-selected language from the cookie. Content may be
  // coerced to English for languages without translations (es), but the
  // pill should still reflect what the user picked.
  const active = (typeof document !== 'undefined' &&
    (document.cookie.match(/(?:^|;)\s*awo_lang=(\w+)/) || [])[1]) || lang;
  const choose = o => {
    try { document.cookie = 'awo_lang=' + o + ';path=/;max-age=31536000;samesite=lax'; } catch (e) {}
    setLang(o); haptic(10);
  };
  return (
    <div data-interactive="true" style={{
      display: "inline-flex", alignItems: "center", gap: 0,
      border: "1px solid rgba(255,255,255,0.18)",
      borderRadius: 3, overflow: "hidden",
      fontFamily: "'JetBrains Mono', monospace",
      fontSize: 10, letterSpacing: "0.14em",
    }}>
      {opts.map((o, i) => (
        <button key={o}
          onClick={() => choose(o)}
          style={{
            background: active === o ? accent : "transparent",
            color: active === o ? "#fff" : "rgba(255,255,255,0.75)",
            border: "none", padding: "5px 9px", cursor: "pointer",
            fontFamily: "inherit", fontSize: "inherit", letterSpacing: "inherit",
            borderLeft: i ? "1px solid rgba(255,255,255,0.18)" : "none",
          }}
        >{labelFor(o)}</button>
      ))}
    </div>
  );
}

// ============================================================
// Placeholder product image — striped SVG, no real photo yet.
// Per-direction wrappers add their own frame.
// ============================================================
// `image` (a real WooCommerce product photo URL) takes over the frame when
// present; without it we fall back to the striped swatch placeholder. The lot
// stamp + category caption stay in both modes so the editorial framing holds.
function GarmentPlaceholder({ swatch = "#1a1a1a", lot = "LOT-000", label = "garment", ratio = 1.25, image = null }) {
  const hasImage = !!image;
  const stampColor = hasImage ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,0.55)";
  const capColor   = hasImage ? "rgba(255,255,255,0.72)" : "rgba(255,255,255,0.4)";
  const shadow     = hasImage ? "0 1px 6px rgba(0,0,0,0.55)" : "none";
  return (
    <div style={{
      width: "100%", aspectRatio: `1 / ${ratio}`, position: "relative",
      background: hasImage ? "#15110e" : swatch,
      backgroundImage: hasImage
        ? `url("${image}")`
        : `repeating-linear-gradient(180deg, rgba(255,255,255,0.025) 0 1px, transparent 1px 6px)`,
      backgroundSize: hasImage ? "cover" : undefined,
      backgroundPosition: hasImage ? "center" : undefined,
      backgroundRepeat: "no-repeat",
      overflow: "hidden",
    }}>
      {!hasImage && (
        <div style={{
          position: "absolute", inset: 0,
          background: `radial-gradient(circle at 50% 35%, rgba(255,255,255,0.06) 0%, transparent 60%)`,
        }} />
      )}
      <div style={{
        position: "absolute", top: 10, left: 12,
        fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: "0.16em",
        color: stampColor, textTransform: "uppercase", textShadow: shadow,
      }}>{lot}</div>
      <div style={{
        position: "absolute", bottom: 10, left: 12, right: 12, display: "flex", justifyContent: "space-between",
        fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: "0.16em",
        color: capColor, textTransform: "uppercase", textShadow: shadow,
      }}>
        <span>[ {label} ]</span>
        <span>{hasImage ? "" : "placeholder"}</span>
      </div>
    </div>
  );
}

// ============================================================
// Logo — inline AWOTOWA wordmark. Loads the SVG file once and
// injects its inner content into a host <svg> so color is
// controllable via CSS `color` / currentColor (mask-image was
// flaky because the SVG's currentColor inside a mask context
// resolves to black, killing luminance masks).
// ============================================================
// Logo source URLs — tries local first (works in preview), falls back to the
// WordPress Media Library URL (works once deployed to awotowa.com).
// If you upload the SVG in a different month, update the WP URL below.
const LOGO_SOURCES = [
  "assets/logo.svg",
  "https://awotowa.com/wp-content/uploads/2026/05/logo.svg",
];

async function fetchLogoSvg() {
  // Production: PHP template injects the SVG inner content as a global.
  if (typeof window !== 'undefined' && window.AWOTOWA_LOGO_SVG_INNER) {
    return '<svg>' + window.AWOTOWA_LOGO_SVG_INNER + '</svg>';
  }
  for (const url of LOGO_SOURCES) {
    try {
      const r = await fetch(url);
      if (r.ok) return await r.text();
    } catch (e) { /* try next */ }
  }
  return null;
}

function Logo({ color = "currentColor", height, width, style, ariaLabel = "AWOTOWA" }) {
  const [inner, setInner] = useState(null);
  useEffect(() => {
    let cancelled = false;
    fetchLogoSvg().then(t => {
      if (cancelled || !t) return;
      // Extract inner content between the outer <svg ...>...</svg>.
      const m = t.match(/<svg[^>]*>([\s\S]*)<\/svg>\s*$/i);
      setInner(m ? m[1] : t);
    });
    return () => { cancelled = true; };
  }, []);
  // height/width may be a number (px) or any CSS value (clamp(), %, etc.)
  const sizing = height != null
    ? { height, width: "auto" }
    : width != null
    ? { width, height: "auto" }
    : { height: 24, width: "auto" };
  return (
    <svg
      viewBox="0 0 1506 696"
      role="img"
      aria-label={ariaLabel}
      style={{ color, display: "block", ...sizing, ...style }}
      dangerouslySetInnerHTML={inner ? { __html: inner } : undefined}
    />
  );
}
function formatPrice(n, lang = "en") {
  return new Intl.NumberFormat(lang === "no" ? "nb-NO" : "en-US", { style: "currency", currency: "NOK", maximumFractionDigits: 0 }).format(n);
}

// ============================================================
// useCart — toy state
// ============================================================
function useCart() {
  const [items, setItems] = useState({});
  const add = (id) => { setItems(s => ({ ...s, [id]: (s[id] || 0) + 1 })); haptic(14); };
  const remove = (id) => { setItems(s => { const c = { ...s }; delete c[id]; return c; }); haptic(8); };
  const total = Object.values(items).reduce((a, b) => a + b, 0);
  return { items, add, remove, total };
}

// ============================================================
// Smooth section scroller helper (for nav anchors)
// ============================================================
function scrollToId(id) {
  const el = document.getElementById(id);
  if (!el) return;
  const top = el.getBoundingClientRect().top + window.scrollY - 80;
  window.scrollTo({ top, behavior: "smooth" });
}

// ============================================================
// Export to window for other Babel script files to consume.
// ============================================================
Object.assign(window, {
  haptic, txField, useTick, useCountdown,
  Redacted, RedactedRich, CustomCursor, LangToggle, Logo,
  GarmentPlaceholder, formatPrice, useCart, scrollToId,
});
