/* =========================================================================
 *  CTWCAD — UI primitives, theme, helpers, thumbnails
 * =======================================================================*/

const { useState, useEffect, useRef, useMemo, useLayoutEffect, useCallback, createContext, useContext } = React;
const { motion, AnimatePresence, useMotionValue, useSpring, useTransform } = window.FramerMotion || {};

/* The brandmark's outer rounded square as a single open <path> with a
 * deliberate ~8-unit overshoot past its starting point at the top edge.
 * Defined here (loads first) so any later component can use it.
 *
 * Why no `Z` and why overshoot:
 *   When framer-motion animates pathLength: 1, browsers render the
 *   stroke-dasharray with subpixel precision that can leave a hair gap
 *   at the seam where the path's last point meets its first. Stroke-
 *   linecap="round" doesn't always rescue that — the ends of the dash
 *   land on top of each other and antialias inconsistently. By making
 *   the path continue past its starting point, the final segment of
 *   the stroke OVERLAPS the first segment, fully covering any seam.
 *   The overshoot is on the top edge so it's symmetric with the
 *   starting corner. */
const ROUNDED_RECT_PATH =
  "M 9 3 L 23 3 A 6 6 0 0 1 29 9 L 29 23 A 6 6 0 0 1 23 29 " +
  "L 9 29 A 6 6 0 0 1 3 23 L 3 9 A 6 6 0 0 1 9 3 L 17 3";

/* ---------- format helpers ---------------------------------------------*/
function fmtBytes(n) {
  if (n == null) return "—";
  if (n < 1024) return n + " B";
  if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
  if (n < 1024 ** 3) return (n / 1024 ** 2).toFixed(1) + " MB";
  return (n / 1024 ** 3).toFixed(2) + " GB";
}
function fmtRelative(iso) {
  const d = new Date(iso);
  const diff = Date.now() - d.getTime();
  const m = Math.round(diff / 60000);
  if (m < 1) return "just now";
  if (m < 60) return m + "m ago";
  const h = Math.round(m / 60);
  if (h < 24) return h + "h ago";
  const day = Math.round(h / 24);
  if (day < 7) return day + "d ago";
  return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
}
function fmtDate(iso) {
  return new Date(iso).toLocaleString(undefined, {
    month: "short", day: "numeric", year: "numeric",
    hour: "numeric", minute: "2-digit",
  });
}

/* ---------- theme -------------------------------------------------------*/
// `mode` is the user's stored preference: "light" | "dark" | "auto".
// `theme` is the *resolved* light/dark we apply to <html data-theme="…">.
// In auto mode we follow `prefers-color-scheme` and re-resolve when the OS
// flips. Existing components only consume `theme` (not `mode`) so legacy
// callsites — CADThumb, the top-bar icon — keep working unchanged.
const ThemeCtx = createContext({
  theme: "dark", mode: "dark",
  toggle: () => {}, setMode: () => {},
});
function resolveTheme(mode) {
  if (mode === "light" || mode === "dark") return mode;
  // mode === "auto" — fall back to OS preference.
  if (typeof window !== "undefined" && window.matchMedia) {
    return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
  }
  return "dark";
}
function ThemeProvider({ children }) {
  const [mode, setModeState] = useState(() => {
    const stored = localStorage.getItem("ctwcad.theme");
    if (stored === "light" || stored === "dark" || stored === "auto") return stored;
    return "dark";
  });
  const [theme, setTheme] = useState(() => resolveTheme(localStorage.getItem("ctwcad.theme") || "dark"));

  // Apply the resolved theme to <html> and persist the user's mode pick.
  useEffect(() => {
    const resolved = resolveTheme(mode);
    setTheme(resolved);
    document.documentElement.dataset.theme = resolved;
    localStorage.setItem("ctwcad.theme", mode);
  }, [mode]);

  // While in "auto", subscribe to OS theme flips so the UI follows along
  // without a refresh. Re-runs only when the user toggles auto on/off.
  useEffect(() => {
    if (mode !== "auto" || !window.matchMedia) return;
    const mq = window.matchMedia("(prefers-color-scheme: light)");
    const onChange = () => {
      const t = mq.matches ? "light" : "dark";
      setTheme(t);
      document.documentElement.dataset.theme = t;
    };
    if (mq.addEventListener) mq.addEventListener("change", onChange);
    else if (mq.addListener) mq.addListener(onChange); // legacy Safari
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", onChange);
      else if (mq.removeListener) mq.removeListener(onChange);
    };
  }, [mode]);

  const setMode = useCallback((nextMode, evt) => {
    if (nextMode !== "light" && nextMode !== "dark" && nextMode !== "auto") return;
    if (nextMode === mode) return;
    const el = evt && evt.currentTarget;
    if (document.startViewTransition && el) {
      const r = el.getBoundingClientRect();
      const cx = r.left + r.width / 2;
      const cy = r.top + r.height / 2;
      const radius = Math.hypot(window.innerWidth, window.innerHeight);
      document.documentElement.style.setProperty("--reveal-x", cx + "px");
      document.documentElement.style.setProperty("--reveal-y", cy + "px");
      document.documentElement.style.setProperty("--reveal-r", radius + "px");
      document.startViewTransition(() => setModeState(nextMode));
    } else {
      setModeState(nextMode);
    }
  }, [mode]);

  // Legacy two-state toggle. Cycles light↔dark and forces an explicit
  // pick (drops out of auto). Existing top-bar / profile-menu callsites
  // hit this with no changes.
  const toggle = useCallback((evt) => {
    const target = theme === "dark" ? "light" : "dark";
    setMode(target, evt);
  }, [theme, setMode]);

  return <ThemeCtx.Provider value={{ theme, mode, toggle, setMode }}>{children}</ThemeCtx.Provider>;
}
const useTheme = () => useContext(ThemeCtx);

/* ---------- icon (Lucide via CDN) --------------------------------------*/
function Icon({ name, size = 16, strokeWidth = 1.5, className = "" }) {
  const ref = useRef();
  useEffect(() => {
    if (!window.lucide || !ref.current) return;
    ref.current.innerHTML = "";
    const svg = window.lucide.createElement(window.lucide.icons[toPascal(name)] || window.lucide.icons.Circle);
    svg.setAttribute("width", size);
    svg.setAttribute("height", size);
    svg.setAttribute("stroke-width", strokeWidth);
    ref.current.appendChild(svg);
  }, [name, size, strokeWidth]);
  return <span ref={ref} className={"ct-icon " + className} style={{ display: "inline-flex", width: size, height: size }} />;
}
function toPascal(s) {
  return s.split("-").map(p => p[0].toUpperCase() + p.slice(1)).join("");
}

/* ---------- procedural CAD thumbnail ----------------------------------*/
// Renders a deterministic, technical-looking SVG schematic per file.
// No stock photos, no AI imagery — just blueprint vibes.
function CADThumb({ file, large = false }) {
  // If the desktop app uploaded a real ISO-view cover screenshot for
  // this file, that's the truth — show it. Falls back to the procedural
  // schematic for files without a cover (web uploads, raw STEP imports,
  // mock fixtures).
  if (file && file.coverDownloadURL) {
    return (
      <img
        src={file.coverDownloadURL}
        alt=""
        className="ct-thumb-cover"
        loading="lazy"
        draggable="false"
      />
    );
  }
  const seed = (file.thumbSeed || 1) + file.id.charCodeAt(file.id.length - 1);
  const rng = mulberry32(seed);
  const W = 400, H = 280;
  const kind = file.kind;
  const { theme } = useTheme();
  const ink = theme === "dark" ? "rgba(220,235,250,0.78)" : "rgba(20,30,45,0.78)";
  const dim = theme === "dark" ? "rgba(220,235,250,0.22)" : "rgba(20,30,45,0.30)";
  const accent = "var(--ct-accent)";

  const elements = useMemo(() => buildSchematic(kind, rng, W, H), [seed, kind]);

  return (
    <svg viewBox={`0 0 ${W} ${H}`} className="ct-thumb-svg" preserveAspectRatio="xMidYMid meet">
      <defs>
        <pattern id={`grid-${seed}`} width="20" height="20" patternUnits="userSpaceOnUse">
          <path d="M 20 0 L 0 0 0 20" fill="none" stroke={dim} strokeWidth="0.4" opacity="0.45" />
        </pattern>
      </defs>
      <rect width={W} height={H} fill={`url(#grid-${seed})`} opacity="0.55" />
      {/* corner ticks */}
      <g stroke={dim} strokeWidth="0.8">
        <path d="M8 8 L20 8 M8 8 L8 20" />
        <path d={`M${W-8} 8 L${W-20} 8 M${W-8} 8 L${W-8} 20`} />
        <path d={`M8 ${H-8} L20 ${H-8} M8 ${H-8} L8 ${H-20}`} />
        <path d={`M${W-8} ${H-8} L${W-20} ${H-8} M${W-8} ${H-8} L${W-8} ${H-20}`} />
      </g>
      <g stroke={ink} strokeWidth="1.1" fill="none">
        {elements.lines.map((l, i) => <path key={"l" + i} d={l} />)}
      </g>
      <g stroke={accent} strokeWidth="1.1" fill="none" opacity="0.95">
        {elements.accents.map((l, i) => <path key={"a" + i} d={l} />)}
      </g>
      <g stroke={dim} strokeWidth="0.6" fill="none">
        {elements.dims.map((l, i) => <path key={"d" + i} d={l} />)}
      </g>
      <g fill={dim} fontSize="7" fontFamily="var(--ct-mono)">
        {elements.labels.map((t, i) => <text key={"t" + i} x={t.x} y={t.y}>{t.s}</text>)}
      </g>
      {/* file kind chip */}
      <g>
        <rect x={W - 56} y={H - 22} width="48" height="14" rx="3"
              fill={theme === "dark" ? "rgba(8,14,22,0.7)" : "rgba(255,255,255,0.7)"}
              stroke={dim} />
        <text x={W - 32} y={H - 12} textAnchor="middle" fontFamily="var(--ct-mono)"
              fontSize="8" fill={ink} letterSpacing="0.05em">.{kind.toUpperCase()}</text>
      </g>
    </svg>
  );
}

function mulberry32(a) {
  return function () {
    let t = (a += 0x6D2B79F5);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function buildSchematic(kind, rng, W, H) {
  const cx = W / 2, cy = H / 2;
  const lines = [], accents = [], dims = [], labels = [];

  if (kind === "ctws") {
    // 2D parametric sketch — rectangle w/ holes & dimensions
    const w = 160 + rng() * 60;
    const h = 90 + rng() * 30;
    const x0 = cx - w / 2, y0 = cy - h / 2;
    lines.push(`M${x0} ${y0} h${w} v${h} h${-w} z`);
    // bolt holes
    const r = 6;
    [[x0 + 16, y0 + 16], [x0 + w - 16, y0 + 16], [x0 + 16, y0 + h - 16], [x0 + w - 16, y0 + h - 16]].forEach(([x, y]) => {
      lines.push(circle(x, y, r));
      accents.push(`M${x - r - 2} ${y} h${r * 2 + 4} M${x} ${y - r - 2} v${r * 2 + 4}`);
    });
    // slot
    accents.push(roundedSlot(cx - 30, cy, 60, 10));
    // dimension lines
    dims.push(`M${x0} ${y0 - 12} h${w}`);
    dims.push(`M${x0} ${y0 - 16} v8 M${x0 + w} ${y0 - 16} v8`);
    labels.push({ x: cx - 10, y: y0 - 14, s: w.toFixed(1) });
    dims.push(`M${x0 + w + 12} ${y0} v${h}`);
    dims.push(`M${x0 + w + 8} ${y0} h8 M${x0 + w + 8} ${y0 + h} h8`);
    labels.push({ x: x0 + w + 16, y: cy + 2, s: h.toFixed(1) });
  } else if (kind === "step" || kind === "stp") {
    // Isometric block with hole
    const u = 70 + rng() * 30;
    const sx = cx - u, sy = cy - u * 0.4;
    const k = 0.5; // iso skew
    // top face
    lines.push(`M${sx} ${sy} L${sx + u} ${sy - u * k} L${sx + u * 2} ${sy} L${sx + u} ${sy + u * k} z`);
    // left face
    lines.push(`M${sx} ${sy} L${sx} ${sy + u * 0.7} L${sx + u} ${sy + u * k + u * 0.7} L${sx + u} ${sy + u * k} z`);
    // right face
    lines.push(`M${sx + u * 2} ${sy} L${sx + u * 2} ${sy + u * 0.7} L${sx + u} ${sy + u * k + u * 0.7} L${sx + u} ${sy + u * k} z`);
    // hole on top
    accents.push(ellipse(sx + u, sy, 14, 7));
    accents.push(ellipse(sx + u, sy + 12, 14, 7));
    // edge dim
    dims.push(`M${sx} ${sy + u * 0.7 + 14} L${sx + u} ${sy + u * k + u * 0.7 + 14}`);
    labels.push({ x: sx + u * 0.4, y: sy + u * 0.7 + 22, s: u.toFixed(0) + " mm" });
  } else if (kind === "stl") {
    // mesh look — triangulated dome
    const cx2 = cx, cy2 = cy + 20;
    const R = 80 + rng() * 10;
    // base ellipse
    lines.push(ellipse(cx2, cy2, R, R * 0.35));
    // facets
    for (let i = 0; i < 12; i++) {
      const a1 = (i / 12) * Math.PI;
      const a2 = ((i + 1) / 12) * Math.PI;
      const x1 = cx2 - Math.cos(a1) * R, y1 = cy2 - Math.sin(a1) * R * 0.6;
      const x2 = cx2 - Math.cos(a2) * R, y2 = cy2 - Math.sin(a2) * R * 0.6;
      lines.push(`M${x1} ${y1} L${cx2} ${cy2 - R * 0.55} L${x2} ${y2}`);
      lines.push(`M${x1} ${y1} L${x2} ${y2}`);
    }
    accents.push(`M${cx2} ${cy2 - R * 0.55} L${cx2} ${cy2}`);
  } else if (kind === "iges") {
    // curve study
    let d = `M${40} ${H - 60}`;
    for (let i = 1; i <= 20; i++) {
      const x = 40 + (i / 20) * (W - 80);
      const y = H - 60 - Math.sin(i / 3 + rng()) * 40 - i * 2;
      d += ` L${x.toFixed(1)} ${y.toFixed(1)}`;
    }
    lines.push(d);
    accents.push(`M${40} ${H - 60} L${W - 40} ${H - 60}`);
    for (let i = 0; i <= 4; i++) {
      const x = 40 + (i / 4) * (W - 80);
      dims.push(`M${x} ${H - 60} v6`);
    }
  } else if (kind === "pdf") {
    // drawing sheet
    lines.push(`M40 36 h${W - 80} v${H - 72} h${-(W - 80)} z`);
    lines.push(`M${W - 140} ${H - 80} h100 v44 h-100 z`);
    for (let i = 0; i < 4; i++) {
      lines.push(`M${W - 140} ${H - 80 + 11 * (i + 1)} h100`);
    }
    accents.push(`M60 60 h${W - 200} v${H - 160} h${-(W - 200)} z`);
    labels.push({ x: W - 132, y: H - 70, s: "TITLE BLOCK" });
    labels.push({ x: W - 132, y: H - 60, s: "REV  03" });
    labels.push({ x: W - 132, y: H - 50, s: "SCALE 1:1" });
    labels.push({ x: W - 132, y: H - 40, s: "A3" });
  } else {
    // generic
    lines.push(`M${cx - 60} ${cy} a60 60 0 1 0 120 0 a60 60 0 1 0 -120 0`);
  }
  return { lines, accents, dims, labels };
}
function circle(x, y, r) { return `M${x - r} ${y} a${r} ${r} 0 1 0 ${r * 2} 0 a${r} ${r} 0 1 0 ${-r * 2} 0`; }
function ellipse(x, y, rx, ry) { return `M${x - rx} ${y} a${rx} ${ry} 0 1 0 ${rx * 2} 0 a${rx} ${ry} 0 1 0 ${-rx * 2} 0`; }
function roundedSlot(x, y, w, h) {
  return `M${x} ${y - h / 2} h${w} a${h / 2} ${h / 2} 0 0 1 0 ${h} h${-w} a${h / 2} ${h / 2} 0 0 1 0 ${-h}`;
}

/* ---------- avatar ----------------------------------------------------*/
function Avatar({ user, size = 28 }) {
  const hue = user?.avatarHue ?? 200;
  return (
    <div className="ct-avatar" style={{
      width: size, height: size,
      background: `linear-gradient(135deg, oklch(0.72 0.10 ${hue}), oklch(0.55 0.13 ${hue + 30}))`,
      fontSize: size * 0.42,
    }}>
      {user?.initials || "?"}
    </div>
  );
}

/* ---------- file kind icon -------------------------------------------*/
function KindBadge({ kind }) {
  const k = kind.toUpperCase();
  return <span className={`ct-kind ct-kind-${kind}`}>{k}</span>;
}

/* =========================================================================
 *  LoadingIndicator — top-left brandmark whose outer border IS the
 *  progress bar for in-flight downloads / part loads.
 *
 *  Listens for window events:
 *    ctwcad:part-loading { progress: 0..1 } → determinate fill
 *    ctwcad:part-loading { progress: null }  → indeterminate (sweeps)
 *    ctwcad:part-loading { done: true }      → fade out
 *
 *  Sits where the topbar's static brandmark renders, so on the dashboard
 *  it visually replaces the brand while loading and disappears when
 *  done, leaving the static brand intact.
 * =======================================================================*/
function LoadingIndicator() {
  const M = window.FramerMotion?.motion;
  const A = window.FramerMotion?.AnimatePresence;
  const [state, setState] = useState({ visible: false, progress: null });

  useEffect(() => {
    let hideTimer;
    const onLoading = (e) => {
      const detail = e.detail || {};
      clearTimeout(hideTimer);
      if (detail.done) {
        // Snap to full, fade out a moment later so the user sees the bar complete.
        setState({ visible: true, progress: 1 });
        hideTimer = setTimeout(() => setState({ visible: false, progress: null }), 450);
        return;
      }
      const p = (typeof detail.progress === "number") ? detail.progress : null;
      setState({ visible: true, progress: p });
    };
    window.addEventListener("ctwcad:part-loading", onLoading);
    return () => {
      window.removeEventListener("ctwcad:part-loading", onLoading);
      clearTimeout(hideTimer);
    };
  }, []);

  if (!M || !A) return null;
  const isDeterminate = state.progress != null;

  return (
    <A>
      {state.visible && (
        <M.div
          className="ct-loading-indicator"
          initial={{ opacity: 0, scale: 0.85 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: 0.92 }}
          transition={{ duration: 0.18 }}
          aria-hidden="true"
          role="progressbar"
        >
          <svg width="36" height="36" viewBox="0 0 32 32">
            {/* Faint base ring so the user sees the shape even at 0%.
                Using <rect> (not the overshoot path) keeps the static
                ring's stroke uniform — no double-paint where the path
                overlaps itself. */}
            <rect x="3" y="3" width="26" height="26" rx="6"
                  fill="none"
                  stroke="var(--ct-border)"
                  strokeWidth="1.5"
                  opacity="0.5"/>
            {/* The progress border itself */}
            {isDeterminate ? (
              <M.path d={ROUNDED_RECT_PATH}
                fill="none" stroke="var(--ct-accent)" strokeWidth="1.8"
                strokeLinecap="round" strokeLinejoin="round"
                initial={{ pathLength: 0 }}
                animate={{ pathLength: state.progress }}
                transition={{ duration: 0.25, ease: "linear" }}
              />
            ) : (
              <M.path d={ROUNDED_RECT_PATH}
                fill="none" stroke="var(--ct-accent)" strokeWidth="1.8"
                strokeLinecap="round" strokeLinejoin="round"
                strokeDasharray="0.35 0.65"
                pathLength={1}
                animate={{ strokeDashoffset: [0, -1] }}
                transition={{ duration: 1.4, repeat: Infinity, ease: "linear" }}
              />
            )}
            <path d="M 10 22 L 10 10 L 22 10"
                  fill="none" stroke="var(--ct-accent)" strokeWidth="1.6"
                  strokeLinecap="round" strokeLinejoin="round" opacity="0.85"/>
            <circle cx="22" cy="22" r="3" fill="var(--ct-accent)" opacity="0.85"/>
          </svg>
        </M.div>
      )}
    </A>
  );
}

/* expose */
Object.assign(window, {
  fmtBytes, fmtRelative, fmtDate,
  ThemeProvider, ThemeCtx, useTheme,
  Icon, CADThumb, Avatar, KindBadge,
  LoadingIndicator, ROUNDED_RECT_PATH,
});
