/* map.jsx — TopBar, GraphCanvas (pan/zoom node map), JourneyRail, Inspector, MapScreen */
/* React hooks (useState/useEffect/useRef/useMemo/useCallback) come from window (set in phone.jsx) */

/* ----------------------------- icons ----------------------------- */
function Icon({ name, s = 16, c = "currentColor", sw = 1.6 }) {
  const P = {
    search: "M11 11l4 4M12.5 7.5a5 5 0 1 1-10 0 5 5 0 0 1 10 0Z",
    plus: "M8 3v10M3 8h10", minus: "M3 8h10",
    expand: "M5 2H2v3M14 5V2h-3M11 14h3v-3M2 11v3h3",
    x: "M4 4l8 8M12 4l-8 8", chev: "M6 4l4 4-4 4",
    sun: "M8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M3 3l1 1M12 12l1 1M13 3l-1 1M4 12l-1 1",
    layers: "M8 2 1.5 5.5 8 9l6.5-3.5L8 2ZM2 9l6 3.3L14 9M2 12l6 3.3L14 12",
    link: "M6.5 9.5 9.5 6.5M6 11l-1 1a2.5 2.5 0 0 1-3.5-3.5l1.5-1.5a2.5 2.5 0 0 1 3.5 0M10 5l1-1a2.5 2.5 0 0 1 3.5 3.5L13 9a2.5 2.5 0 0 1-3.5 0",
    sparkle: "M8 1.5l1.6 4.3 4.3 1.6-4.3 1.6L8 13.3 6.4 9 2 7.4l4.4-1.6L8 1.5Z",
    play: "M5 3.5v9l7-4.5-7-4.5Z", check: "M3 8.5l3 3 6-7",
    grid: "M2 2h5v5H2V2ZM9 2h5v5H9V2ZM2 9h5v5H2V9ZM9 9h5v5H9V9Z",
    flow: "M3 4h4M3 8h7M3 12h4M12 6.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM12 12.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z",
    arrow: "M3 8h9M9 5l3 3-3 3", dl: "M8 2v8M5 7l3 3 3-3M3 13h10",
    phone: "M5 1.5h6a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-11a1 1 0 0 1 1-1ZM7 12.5h2",
    dot: "M8 8m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0",
    star: "M8 2l1.7 3.6 3.8.5-2.8 2.7.7 3.9L8 10.9 4.6 12.7l.7-3.9L2.5 6.1l3.8-.5L8 2Z",
  };
  const d = P[name] || "";
  return (
    <svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke={c} strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
      {d.split("M").filter(Boolean).map((seg, i) => <path key={i} d={"M" + seg} />)}
    </svg>
  );
}

/* ----------------------------- brand + topbar ----------------------------- */
function BrandMark({ s = 22 }) {
  return (
    <div style={{ width: s, height: s, position: "relative", flex: "none" }}>
      <svg width={s} height={s} viewBox="0 0 24 24" fill="none">
        <rect x="1" y="1" width="22" height="22" rx="6" fill="#0e0e12" stroke="var(--acc-dim)" />
        <path d="M7 16.5 11 6l2 6 1.5-3L17 16.5" stroke="var(--acc-hi)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />
        <circle cx="11" cy="6" r="1.5" fill="var(--acc-hi)" />
        <circle cx="14.5" cy="9" r="1.2" fill="var(--n-pink)" />
        <circle cx="17" cy="16.5" r="1.3" fill="var(--n-green)" />
      </svg>
    </div>
  );
}

function Breadcrumb({ stack }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
      {stack.map((s, i) => {
        const last = i === stack.length - 1;
        const link = !!s.go && !last;
        return (
          <React.Fragment key={i}>
            {i > 0 && <span style={{ color: "var(--t-faint)", display: "inline-flex" }}><Icon name="chev" s={11} /></span>}
            <span onClick={link ? s.go : undefined}
              onMouseEnter={e => { if (link) { e.currentTarget.style.color = "var(--t-hi)"; e.currentTarget.style.background = "var(--hover)"; } }}
              onMouseLeave={e => { if (link) { e.currentTarget.style.color = "var(--t-dim)"; e.currentTarget.style.background = "var(--panel-2)"; } }}
              style={{ fontFamily: "var(--mono)", fontSize: 11.5, padding: "4px 9px", borderRadius: 6, whiteSpace: "nowrap",
                cursor: link ? "pointer" : "default",
                color: last ? "var(--acc-hi)" : "var(--t-dim)",
                background: last ? "var(--acc-soft)" : "var(--panel-2)",
                border: `1px solid ${last ? "rgba(139,124,246,.3)" : "var(--line)"}`,
                transition: "color .15s, background .15s" }}>{s.label}</span>
          </React.Fragment>
        );
      })}
    </div>
  );
}

function TopBar({ app, stats, tab, setTab, onHome, onOpenDetail, theme, toggleTheme }) {
  const crumbs = [
    { label: "首页", go: onHome },
    { label: app.name, go: onOpenDetail },
    { label: "地图", go: null },
  ];
  return (
    <div className="topbar">
      <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
        <div onClick={onHome} style={{ display: "flex", alignItems: "center", gap: 9, cursor: "pointer" }}>
          <BrandMark />
          <span className="crumb"><span className="brand">CARTO</span></span>
        </div>
        <span className="crumb" style={{ color: "var(--t-faint)" }}>/</span>
        <Breadcrumb stack={crumbs} />
      </div>

      <div style={{ flex: 1 }} />

      <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
        <div className="tabs">
          {[["map", "MAP"], ["screens", `SCREENS · ${stats.screens}`], ["report", "REPORT"]].map(([k, l]) => (
            <div key={k} className={"tab" + (tab === k ? " on" : "")} onClick={() => setTab(k)}>{l}</div>
          ))}
        </div>
        <div className="icon-btn" onClick={toggleTheme} title="主题"><Icon name="sun" s={15} /></div>
      </div>
    </div>
  );
}

function AppGlyph({ app, s = 20 }) {
  return (
    <div style={{ width: s, height: s, borderRadius: s * 0.27, background: app.icon.bg, display: "grid", placeItems: "center", flex: "none", boxShadow: "inset 0 0 0 1px rgba(255,255,255,.12)" }}>
      <span style={{ fontSize: s * 0.6, lineHeight: 1 }}>{app.icon.fg || ""}</span>
    </div>
  );
}

/* ----------------------------- graph canvas ----------------------------- */
const NODE_W = 58;
const NODE_H = Math.round(NODE_W * 2.04);
const MIN_Z = 0.25, MAX_Z = 5;   // 缩放区间：上限调高，可放得更大

/* 空间方位最近节点：从 fromId 出发，沿 dir 方向锥形范围内挑最近的可见节点 */
function nearestInDir(visNodes, fromId, dir) {
  const cur = visNodes.find(n => n.id === fromId);
  if (!cur) return null;
  const cx = cur.x + NODE_W / 2, cy = cur.y + NODE_H / 2;
  let best = null, bestScore = Infinity;
  for (const n of visNodes) {
    if (n.id === fromId) continue;
    const dx = (n.x + NODE_W / 2) - cx, dy = (n.y + NODE_H / 2) - cy;
    let along, perp;
    if (dir === "right") { along = dx; perp = Math.abs(dy); }
    else if (dir === "left") { along = -dx; perp = Math.abs(dy); }
    else if (dir === "down") { along = dy; perp = Math.abs(dx); }
    else { along = -dy; perp = Math.abs(dx); }   // up
    if (along <= 1) continue;                     // 必须确实在该方向
    if (perp > along * 1.6) continue;             // 限制在约 ±58° 锥角内
    const score = along + perp * 2;               // 越正对、越近越优先
    if (score < bestScore) { bestScore = score; best = n.id; }
  }
  return best;
}

function GraphCanvas({ nodes, edges, visible, selected, onSelect, tw, grow, controls = true, fitSignal, dimUnselected, onResetJourney }) {
  const wrapRef = useRef(null);
  const [view, setView] = useState({ x: 0, y: 0, z: 0.72 });
  const [anim, setAnim] = useState(false);   // true 时 transform 走 CSS 过渡（选中/复位），拖拽缩放时为 false
  const animRef = useRef(null);
  const dragRef = useRef(null);
  const movedRef = useRef(false);
  const prevSelRef = useRef(null);
  const nodeMap = useMemo(() => Object.fromEntries(nodes.map(n => [n.id, n])), [nodes]);

  const startAnim = useCallback(() => {
    setAnim(true);
    if (animRef.current) clearTimeout(animRef.current);
    animRef.current = setTimeout(() => setAnim(false), 480);
  }, []);
  const stopAnim = useCallback(() => {
    if (animRef.current) { clearTimeout(animRef.current); animRef.current = null; }
    setAnim(false);
  }, []);

  const boundsOf = useCallback((ns) => {
    const xs = ns.map(n => n.x), ys = ns.map(n => n.y);
    const minX = Math.min(...xs) - 60, minY = Math.min(...ys) - 60;
    const maxX = Math.max(...xs) + NODE_W + 60, maxY = Math.max(...ys) + NODE_H + 60;
    return { minX, minY, maxX, maxY, w: maxX - minX, h: maxY - minY };
  }, []);

  // 全图边界（SVG 画布定位用，永远覆盖所有节点）
  const bounds = useMemo(() => boundsOf(nodes), [nodes, boundsOf]);
  // 可见边界：选了某条路径时只框住该路径的节点，否则等于全图 —— 取景以此为准
  const visBounds = useMemo(() => {
    const set = visible ? (visible instanceof Set ? visible : new Set(visible)) : null;
    const vn = set ? nodes.filter(n => set.has(n.id)) : nodes;
    return vn.length ? boundsOf(vn) : bounds;
  }, [visible, nodes, bounds, boundsOf]);

  const fit = useCallback((b = visBounds) => {
    const el = wrapRef.current; if (!el) return;
    const vw = el.clientWidth, vh = el.clientHeight;
    const z = Math.min(vw / b.w, vh / b.h) * 0.92;
    setView({ z, x: (vw - b.w * z) / 2 - b.minX * z, y: (vh - b.h * z) / 2 - b.minY * z });
  }, [visBounds]);

  useEffect(() => { fit(); /* eslint-disable-next-line */ }, [fitSignal]);
  useEffect(() => { const t = setTimeout(fit, 60); return () => clearTimeout(t); }, []);

  // 点击左侧路径 → 以「能完整框住该路径的最大预览形态」居中（类似点击节点居中）。
  // 切回「全部」也平滑复位到全预览。首帧 prevVisRef===visible，不抢占挂载时的初始 fit。
  const prevVisRef = useRef(visible);
  useEffect(() => {
    if (prevVisRef.current === visible) return;
    prevVisRef.current = visible;
    startAnim();
    fit(visBounds);
    // eslint-disable-next-line
  }, [visible, visBounds]);

  // 选中 → 动画聚焦进节点；从选中态退回 → 动画复位到全预览（fit）。两者都走过渡，不硬切。
  useEffect(() => {
    const el = wrapRef.current; if (!el) return;
    if (selected && nodeMap[selected]) {
      const n = nodeMap[selected];
      const cx = n.x + NODE_W / 2, cy = n.y + NODE_H / 2;
      startAnim();
      setView(v => {
        const vw = el.clientWidth, vh = el.clientHeight;
        const z = Math.min(MAX_Z, Math.max(v.z, 1.15));   // 聚焦时适度放大，形成「形态切换」的层次感
        const sx = vw * 0.42 - cx * z, sy = vh * 0.5 - cy * z;
        return { z, x: sx, y: sy };
      });
    } else if (prevSelRef.current) {
      startAnim();   // 退出选中：平滑回到全预览
      fit();
    }
    prevSelRef.current = selected;
    // eslint-disable-next-line
  }, [selected]);

  // 键盘导航：ESC 回全预览；方向键按空间方位在节点间切换（未选中时方向键先选根节点）。
  useEffect(() => {
    const DIRS = { ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" };
    const onKey = (e) => {
      const t = e.target;
      if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
      // ESC 逐级退出：先取消选中节点；若无选中但已选路径，则清空路径回全预览。
      if (e.key === "Escape") {
        if (selected) { e.preventDefault(); onSelect && onSelect(null); }
        else if (visible && onResetJourney) { e.preventDefault(); onResetJourney(); }
        return;
      }
      const dir = DIRS[e.key];
      if (!dir || !onSelect) return;
      const idset = visible ? (visible instanceof Set ? visible : new Set(visible)) : null;
      const vis = nodes.filter(n => !idset || idset.has(n.id));
      if (!vis.length) return;
      e.preventDefault();
      if (!selected || !nodeMap[selected]) { onSelect(vis[0].id); return; }
      const next = nearestInDir(vis, selected, dir);
      if (next) onSelect(next);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [selected, onSelect, nodes, visible, nodeMap, onResetJourney]);

  // wheel 用原生非被动监听：React 的 onWheel 在 root 上是 passive 的，preventDefault 无效，
  // 会导致触控板捏合/Ctrl+滚轮触发整页缩放。这里 passive:false 才能真正拦下。
  useEffect(() => {
    const el = wrapRef.current; if (!el) return;
    const handler = (e) => {
      e.preventDefault();
      stopAnim();
      const r = el.getBoundingClientRect();
      const mx = e.clientX - r.left, my = e.clientY - r.top;
      setView(v => {
        const nz = Math.min(MAX_Z, Math.max(MIN_Z, v.z * (e.deltaY < 0 ? 1.08 : 0.926)));
        const k = nz / v.z;
        return { z: nz, x: mx - (mx - v.x) * k, y: my - (my - v.y) * k };
      });
    };
    el.addEventListener("wheel", handler, { passive: false });
    return () => el.removeEventListener("wheel", handler);
  }, [stopAnim]);

  const onDown = (e) => { stopAnim(); dragRef.current = { px: e.clientX, py: e.clientY, x: view.x, y: view.y }; movedRef.current = false; };
  const onMove = (e) => {
    if (!dragRef.current) return;
    const dx = e.clientX - dragRef.current.px, dy = e.clientY - dragRef.current.py;
    if (Math.abs(dx) + Math.abs(dy) > 4) movedRef.current = true;
    setView(v => ({ ...v, x: dragRef.current.x + dx, y: dragRef.current.y + dy }));
  };
  const onUp = () => { dragRef.current = null; };

  const zoomBy = (f) => { startAnim(); setView(v => {
    const el = wrapRef.current; const cx = el.clientWidth / 2, cy = el.clientHeight / 2;
    const nz = Math.min(MAX_Z, Math.max(MIN_Z, v.z * f));
    const k = nz / v.z;
    return { z: nz, x: cx - (cx - v.x) * k, y: cy - (cy - v.y) * k };
  }); };
  const animFit = () => { startAnim(); fit(); };

  const idset = visible ? (visible instanceof Set ? visible : new Set(visible)) : null;
  const isVis = (id) => !idset || idset.has(id);
  const showLabels = !tw || tw.labels !== false;

  const edgeStyle = (tw && tw.edge) || "elbow";
  const path = (a, b) => {
    const x1 = a.x + NODE_W / 2, y1 = a.y + NODE_H, x2 = b.x + NODE_W / 2, y2 = b.y;
    if (edgeStyle === "curve") {
      const my = (y1 + y2) / 2;
      return `M${x1},${y1} C${x1},${my} ${x2},${my} ${x2},${y2}`;
    }
    if (edgeStyle === "straight") return `M${x1},${y1} L${x2},${y2}`;
    // elbow with rounded corners
    const my = (y1 + y2) / 2, rr = 8, dir = x2 > x1 ? 1 : -1;
    if (Math.abs(x2 - x1) < 2) return `M${x1},${y1} L${x2},${y2}`;
    return `M${x1},${y1} L${x1},${my - rr} Q${x1},${my} ${x1 + dir * rr},${my} L${x2 - dir * rr},${my} Q${x2},${my} ${x2},${my + rr} L${x2},${y2}`;
  };

  const selNode = selected && nodeMap[selected];
  const connected = useMemo(() => {
    const s = new Set();
    if (!selected) return s;
    edges.forEach(([a, b]) => { if (a === selected) s.add(b); if (b === selected) s.add(a); });
    return s;
  }, [selected, edges]);

  return (
    <div ref={wrapRef} onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerLeave={onUp}
      style={{ position: "absolute", inset: 0, overflow: "hidden", cursor: dragRef.current ? "grabbing" : "grab",
        touchAction: "none",
        background: "var(--canvas)",
        backgroundImage: "radial-gradient(var(--line) 0.5px, transparent 0.5px)", backgroundSize: "26px 26px" }}>
      <div style={{ position: "absolute", left: 0, top: 0, transform: `translate(${view.x}px,${view.y}px) scale(${view.z})`, transformOrigin: "0 0",
        transition: anim ? "transform .46s cubic-bezier(.22,.61,.36,1)" : "none" }}>
        <svg style={{ position: "absolute", left: bounds.minX, top: bounds.minY, overflow: "visible", pointerEvents: "none" }} width={bounds.w} height={bounds.h}>
          <g transform={`translate(${-bounds.minX},${-bounds.minY})`}>
            {edges.map(([a, b], i) => {
              const na = nodeMap[a], nb = nodeMap[b];
              if (!na || !nb || !isVis(a) || !isVis(b)) return null;
              const hot = selected && (a === selected || b === selected);
              return <path key={i} d={path(na, nb)} fill="none"
                stroke={hot ? "var(--acc-hi)" : "var(--acc-dim)"}
                strokeWidth={hot ? 1.8 : 1} strokeOpacity={hot ? 0.95 : (selected ? 0.18 : 0.42)} />;
            })}
          </g>
        </svg>

        {nodes.map(n => {
          if (!isVis(n.id)) return null;
          const isSel = n.id === selected;
          const faded = selected && dimUnselected && !isSel && !connected.has(n.id);
          return (
            <div key={n.id} className={grow ? "pop-in" : ""}
              onClick={(e) => { e.stopPropagation(); if (!movedRef.current) onSelect && onSelect(n.id); }}
              style={{ position: "absolute", left: n.x, top: n.y, cursor: "pointer" }}>
              <Phone arch={n.arch} accent={n.accent} shot={n.shot} w={NODE_W} active={isSel} dim={faded} />
              {showLabels && <div style={{ position: "absolute", top: -16, left: "50%", transform: "translateX(-50%)", whiteSpace: "nowrap",
                fontFamily: "var(--mono)", fontSize: 7.5, letterSpacing: ".04em", color: isSel ? "var(--acc-hi)" : "var(--t-faint)",
                opacity: faded ? 0.3 : 1, transition: "color .2s" }}>{n.name}</div>}
            </div>
          );
        })}
      </div>

      {controls && (
        <div style={{ position: "absolute", left: 18, bottom: 18, display: "flex", flexDirection: "column", gap: 6 }}>
          {[["plus", () => zoomBy(1.2)], ["minus", () => zoomBy(0.83)], ["expand", animFit]].map(([ic, fn], i) => (
            <div key={i} className="icon-btn" onClick={fn}
              style={{ width: 34, height: 34, background: "var(--panel)", border: "1px solid var(--line-2)" }}>
              <Icon name={ic} s={15} />
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ----------------------------- journey rail ----------------------------- */
function JourneyCard({ j, active, onClick }) {
  return (
    <div onClick={onClick} style={{
      padding: "16px 20px", borderBottom: "1px solid var(--line)", cursor: "pointer",
      background: active ? "var(--panel-2)" : "transparent", transition: "background .15s",
      borderLeft: active ? "2px solid var(--acc)" : "2px solid transparent",
    }}
      onMouseEnter={e => { if (!active) e.currentTarget.style.background = "rgba(255,255,255,.02)"; }}
      onMouseLeave={e => { if (!active) e.currentTarget.style.background = "transparent"; }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
        <span className="pill">{j.cat}</span>
        <span className="num" style={{ fontSize: 11, color: "var(--t-dim)" }}>{j.n} 屏</span>
      </div>
      <div style={{ color: "var(--t-hi)", fontWeight: 600, fontSize: 15, marginBottom: 6, letterSpacing: ".005em" }}>{j.title}</div>
      <div style={{ color: "var(--t-dim)", fontSize: 12.5, lineHeight: 1.55, marginBottom: 12, display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{j.desc}</div>
      <div className="seg">{Array.from({ length: j.n }).map((_, i) => <i key={i} className={i < j.fill ? "on" : ""} />)}</div>
    </div>
  );
}

function JourneyRail({ journeys, stats, activeJourney, onPick }) {
  return (
    <div style={{ width: 310, flex: "none", borderRight: "1px solid var(--line)", background: "var(--bg-2)", display: "flex", flexDirection: "column" }}>
      <div style={{ padding: "20px 20px 18px" }}>
        <div className="eyebrow" style={{ fontSize: 12, letterSpacing: ".22em", color: "var(--t-hi)", fontWeight: 600 }}>USER JOURNEYS</div>
        <div style={{ color: "var(--t-dim)", fontSize: 12.5, marginTop: 5 }}>已发现 {journeys.length} 条用户路径</div>
        {stats && (
          <div style={{ display: "flex", marginTop: 16, border: "1px solid var(--line)", borderRadius: 9, overflow: "hidden", background: "var(--panel)" }}>
            {[["屏数", stats.screens], ["路径", stats.paths], ["元素", stats.elements]].map(([k, v], i) => (
              <div key={k} style={{ flex: 1, padding: "11px 6px", textAlign: "center", borderLeft: i ? "1px solid var(--line)" : "none" }}>
                <div className="num" style={{ color: "var(--t-hi)", fontSize: 18, fontWeight: 600 }}>{v}</div>
                <div className="eyebrow" style={{ fontSize: 8.5, marginTop: 4, letterSpacing: ".12em" }}>{k}</div>
              </div>
            ))}
          </div>
        )}
      </div>
      <div style={{ flex: 1, overflowY: "auto", borderTop: "1px solid var(--line)" }}>
        {journeys.map(j => <JourneyCard key={j.id} j={j} active={activeJourney === j.id} onClick={() => onPick(j.id)} />)}
      </div>
    </div>
  );
}

window.MapShared = { Icon, BrandMark, TopBar, AppGlyph, GraphCanvas, JourneyRail, NODE_W, NODE_H };
Object.assign(window, { Icon, BrandMark, TopBar, AppGlyph, GraphCanvas, JourneyRail, Breadcrumb });
