/* Custex editor — canvas pan/zoom, wires, scissors, node drag. */
const { useState, useRef, useEffect, useCallback } = React;
const C = window.CxEditorConstants;
const {
  NODE_W, HEADER_H, CX_PALETTE_DRAG_MIME,
  MOODBOARD_MIN, MOODBOARD_MAX, defaultMoodboardFromGraph,
} = C;
const {
  portPos, fitCanvasToView, findNearestInputPort, allowsMultipleInputs, nextNodeId,
  connectionIndicesCrossedBySlash, polylineLengthWorld,
  FIT_SCALE_MIN, FIT_SCALE_MAX, SCISSOR_SLASH_MIN_LEN,
} = window.CxGeom;
const { portsCompatible, reshapeForCraft } = window.CxGraphModel;

function useCxEditorCanvas({ graph, setGraph, pan, setPan, scale, setScale, selected, setSelected, visibleGraph, route }) {
  const [drag, setDrag] = useState(null);   // node drag: { id, ox, oy }
  const [moodboardDrag, setMoodboardDrag] = useState(null);   // { ox, oy }
  const [moodboardResize, setMoodboardResize] = useState(null); // { startW, startH, startMx, startMy }
  const [connect, setConnect] = useState(null); // { fromId, fromPort, fromType, x, y, hover? }
  /** null = idle (pan only) · 'needle' = connect · 'scissors' = click wire to remove — cleared on empty-canvas click */
  const [wireTool, setWireTool] = useState(null);
  const [panning, setPanning] = useState(null);
  const wrapRef = useRef(null);
  const scissorStrokeRef = useRef(null);
  const [scissorStroke, setScissorStroke] = useState(null);
  const graphRef = useRef(graph);
  graphRef.current = graph;
  // On refresh / entering Studio: center the full graph in the canvas with a fitting zoom
  useEffect(() => {
    if (route !== 'studio') return;
    let cancelled = false;
    const run = () => {
      if (cancelled) return;
      const el = wrapRef.current;
      if (!el) {
        requestAnimationFrame(run);
        return;
      }
      const { width, height } = el.getBoundingClientRect();
      if (width < 48 || height < 48) {
        requestAnimationFrame(run);
        return;
      }
      const { pan: p, scale: s } = fitCanvasToView(width, height, graphRef.current.nodes, graphRef.current.moodboard);
      setPan(p);
      setScale(s);
    };
    requestAnimationFrame(() => requestAnimationFrame(run));
    return () => { cancelled = true; };
  }, [route]);

  // ── canvas → world coords
  const toWorld = (e) => {
    const r = wrapRef.current.getBoundingClientRect();
    return {
      x: (e.clientX - r.left - pan.x) / scale,
      y: (e.clientY - r.top  - pan.y) / scale,
    };
  };

  const onCanvasDragOver = useCallback((e) => {
    const types = e.dataTransfer?.types;
    if (!types || !types.length) return;
    const arr = Array.from(types);
    if (!arr.includes(CX_PALETTE_DRAG_MIME) && !arr.includes('text/plain')) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy';
  }, []);

  const onCanvasDrop = useCallback(
    (e) => {
      const templateId =
        e.dataTransfer?.getData(CX_PALETTE_DRAG_MIME) || e.dataTransfer?.getData('text/plain') || '';
      if (!templateId || !graphRef.current.nodes[templateId]) return;
      e.preventDefault();
      e.stopPropagation();
      const el = wrapRef.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      const w = {
        x: (e.clientX - r.left - pan.x) / scale,
        y: (e.clientY - r.top - pan.y) / scale,
      };
      let createdId = null;
      setGraph((g) => {
        const src = g.nodes[templateId];
        if (!src) return g;
        const newId = nextNodeId(g.nodes, src.role);
        createdId = newId;
        const clone = {
          ...JSON.parse(JSON.stringify(src)),
          id: newId,
          x: Math.round(w.x - NODE_W / 2),
          y: Math.round(w.y - HEADER_H / 2),
          hidden: false,
        };
        return { ...g, nodes: { ...g.nodes, [newId]: clone } };
      });
      if (createdId) setSelected(createdId);
    },
    [pan.x, pan.y, scale]
  );

  const resolveMoodboard = useCallback((g) => g.moodboard ?? defaultMoodboardFromGraph(g), []);

  const updateMoodboard = useCallback((patchOrFn) => {
    setGraph((g) => {
      const base = resolveMoodboard(g);
      const patch = typeof patchOrFn === 'function' ? patchOrFn(base) : patchOrFn;
      return { ...g, moodboard: { ...base, ...patch } };
    });
  }, [resolveMoodboard]);

  const onMoodboardMoveStart = (e) => {
    if (e.button !== 0) return;
    e.stopPropagation();
    const mb = resolveMoodboard(graphRef.current);
    const w = toWorld(e);
    setMoodboardDrag({ ox: w.x - mb.x, oy: w.y - mb.y });
    setSelected(null);
  };

  const onMoodboardResizeStart = (e) => {
    if (e.button !== 0) return;
    e.stopPropagation();
    const mb = resolveMoodboard(graphRef.current);
    const w = toWorld(e);
    setMoodboardResize({
      startW: mb.w,
      startH: mb.h,
      startMx: w.x,
      startMy: w.y,
    });
    setSelected(null);
  };

  // ── node drag start (header)
  const onHeaderMouseDown = (e, id) => {
    if (e.button !== 0) return;
    e.stopPropagation();
    const w = toWorld(e);
    const n = graph.nodes[id];
    setDrag({ id, ox: w.x - n.x, oy: w.y - n.y });
    setSelected(id);
  };

  // ── canvas pan: empty-space click drags canvas (left button OR middle / alt)
  const onCanvasMouseDown = (e) => {
    // ignore if clicking node / dock / chrome (scissors: allow slash starting on wires — geometric hit, not click-to-delete)
    if (e.target.closest('.cx-node, .cx-port, .cx-zoom, .cx-minimap, .cx-canvas-br-dock, .cx-canvas-minimap-dock, .cx-hint, .cx-fabric-tag, .cx-moodboard, .cx-moodboard-head, .cx-moodboard-resize, .cx-moodboard-body, .cx-moodboard-canvas, .cx-moodboard-item, .cx-moodboard-item-resize, .cx-moodboard-item-delete, .cx-validation')) return;
    if (wireTool !== 'scissors' && e.target.closest('.cx-wire')) return;
    if (e.button === 1) {
      scissorStrokeRef.current = null;
      setScissorStroke(null);
      setWireTool(null);
      setConnect(null);
      setPanning({ sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y });
      setSelected(null);
      e.preventDefault();
      return;
    }
    if (e.button === 0) {
      if (wireTool === 'scissors') {
        const w = toWorld(e);
        scissorStrokeRef.current = { points: [[w.x, w.y]] };
        setScissorStroke({ points: [[w.x, w.y]] });
        setSelected(null);
        e.preventDefault();
        return;
      }
      scissorStrokeRef.current = null;
      setScissorStroke(null);
      setWireTool(null);
      setConnect(null);
      setPanning({ sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y });
      setSelected(null);
      e.preventDefault();
    }
  };

  // ── port interactions
  const onPortMouseDown = (e, id, port, side, type) => {
    if (wireTool !== 'needle') return;
    if (side !== 'out') return;
    e.stopPropagation();
    const n = graph.nodes[id];
    const p = portPos(n, port, 'out');
    setConnect({ fromId: id, fromPort: port, fromType: type, x: p.x, y: p.y, hover: null });
  };

  const onPortMouseUp = (e, id, port, side, type) => {
    if (!connect || side !== 'in') return;
    e.stopPropagation();
    if (connect.fromId === id) { setConnect(null); return; }
    if (!portsCompatible(connect.fromType, type)) {
      // incompatible — flash and abort
      setConnect(null);
      return;
    }
    setGraph(g => {
      const existing = allowsMultipleInputs(id, port)
        ? g.connections
        : g.connections.filter(c => !(c.to === id && c.toPort === port));
      const alreadyLinked = existing.some(c =>
        c.from === connect.fromId &&
        c.fromPort === connect.fromPort &&
        c.to === id &&
        c.toPort === port
      );
      return {
        ...g,
        connections: alreadyLinked
          ? existing
          : [...existing, { from: connect.fromId, fromPort: connect.fromPort, to: id, toPort: port }],
      };
    });
    setConnect(null);
  };

  // ── update node data
  const updateNode = useCallback((id, patch) => {
    setGraph(g => ({ ...g, nodes: { ...g.nodes, [id]: { ...g.nodes[id], data: { ...g.nodes[id].data, ...patch } } } }));
  }, []);
  const setNodeCollapsed = useCallback((id, collapsed) => {
    setGraph((g) => {
      const n = g.nodes[id];
      if (!n) return g;
      return { ...g, nodes: { ...g.nodes, [id]: { ...n, collapsed } } };
    });
  }, []);
  // patch param sub-object on craft node
  const updateCraftParam = useCallback((key, value) => {
    setGraph(g => ({
      ...g,
      nodes: { ...g.nodes,
        craft: { ...g.nodes.craft, data: { ...g.nodes.craft.data, params: { ...g.nodes.craft.data.params, [key]: value } } } },
    }));
  }, []);

  // ── change craft type — reshapes graph topology
  const changeCraftType = useCallback((newCraftId) => {
    setGraph(g => reshapeForCraft(g, newCraftId));
    setSelected('craft');
  }, []);

  // ── change product (auto-correct downstream constraint violations)
  const changeProduct = useCallback((newProductId) => {
    setGraph(g => {
      const next = { ...g, nodes: { ...g.nodes, product: { ...g.nodes.product, data: { selected: newProductId } } } };
      // auto-fix craft if now disallowed
      const allowedCrafts = window.CxSchema.availableProcesses(newProductId);
      const currentCraft = next.nodes.craft.data.type;
      if (!allowedCrafts.find(c => c.id === currentCraft)) {
        return reshapeForCraft(next, allowedCrafts[0].id);
      }
      // auto-fix material if now disallowed
      if (next.nodes.fabric) {
        const allowedMats = window.CxSchema.availableMaterials(newProductId, currentCraft);
        const currentMat = next.nodes.fabric.data.materialId;
        if (!allowedMats.find(m => m.id === currentMat)) {
          next.nodes = { ...next.nodes, fabric: { ...next.nodes.fabric, data: { materialId: allowedMats[0]?.id || currentMat } } };
        }
      }
      return next;
    });
  }, []);

  // ── delete connection (index into graph.connections — must match render loop)
  const deleteConnection = (i) => {
    setGraph(g => ({ ...g, connections: g.connections.filter((_, idx) => idx !== i) }));
  };

  const setWireToolSafe = (t) => {
    setWireTool(t);
    if (t !== 'needle') setConnect(null);
  };

  const duplicateNode = useCallback((id) => {
    let createdId = null;
    setGraph((g) => {
      const source = g.nodes[id];
      if (!source) return g;
      const newId = nextNodeId(g.nodes, source.role);
      createdId = newId;
      const clone = {
        ...JSON.parse(JSON.stringify(source)),
        id: newId,
        x: source.x + 32,
        y: source.y + 32,
        hidden: false,
      };
      return { ...g, nodes: { ...g.nodes, [newId]: clone } };
    });
    if (createdId) setSelected(createdId);
  }, []);

  const deleteNode = useCallback((id) => {
    setGraph((g) => {
      if (!g.nodes[id]) return g;
      return {
        ...g,
        nodes: { ...g.nodes, [id]: { ...g.nodes[id], hidden: true } },
        connections: g.connections.filter((c) => c.from !== id && c.to !== id),
      };
    });
    setSelected((s) => (s === id ? null : s));
    setDrag((d) => (d?.id === id ? null : d));
    setConnect((c) => (c?.fromId === id ? null : c));
  }, []);

  // ── global mouse move/up
  useEffect(() => {
    const move = (e) => {
      if (scissorStrokeRef.current) {
        const w = toWorld(e);
        const pts = scissorStrokeRef.current.points;
        const [lx, ly] = pts[pts.length - 1];
        if (Math.hypot(w.x - lx, w.y - ly) < 2.2) return;
        pts.push([w.x, w.y]);
        setScissorStroke({ points: [...pts] });
        return;
      }
      if (drag) {
        const w = toWorld(e);
        setGraph(g => ({
          ...g,
          nodes: { ...g.nodes, [drag.id]: { ...g.nodes[drag.id], x: w.x - drag.ox, y: w.y - drag.oy } },
        }));
      }
      if (moodboardDrag) {
        const w = toWorld(e);
        setGraph((g) => ({
          ...g,
          moodboard: {
            ...resolveMoodboard(g),
            x: Math.round(w.x - moodboardDrag.ox),
            y: Math.round(w.y - moodboardDrag.oy),
          },
        }));
      }
      if (moodboardResize) {
        const w = toWorld(e);
        const dw = w.x - moodboardResize.startMx;
        const dh = w.y - moodboardResize.startMy;
        const nw = Math.min(MOODBOARD_MAX, Math.max(MOODBOARD_MIN, moodboardResize.startW + dw));
        const nh = Math.min(MOODBOARD_MAX, Math.max(MOODBOARD_MIN, moodboardResize.startH + dh));
        setGraph((g) => ({
          ...g,
          moodboard: {
            ...resolveMoodboard(g),
            w: Math.round(nw),
            h: Math.round(nh),
          },
        }));
      }
      if (connect) {
        const w = toWorld(e);
        const hover = findNearestInputPort(visibleGraph, w.x, w.y, connect.fromId);
        const valid = hover && portsCompatible(connect.fromType, hover.type);
        setConnect(c => ({ ...c, x: w.x, y: w.y, hover: valid ? hover : null, hoverInvalid: hover && !valid ? hover : null }));
      }
      if (panning) {
        setPan({ x: panning.px + (e.clientX - panning.sx), y: panning.py + (e.clientY - panning.sy) });
      }
    };
    const up = () => {
      if (scissorStrokeRef.current) {
        const pts = scissorStrokeRef.current.points;
        scissorStrokeRef.current = null;
        setScissorStroke(null);
        const len = polylineLengthWorld(pts);
        if (pts.length < 2 || len < SCISSOR_SLASH_MIN_LEN) {
          setWireTool((w) => (w === 'scissors' ? null : w));
        } else {
          const g = graphRef.current;
          const vis = Object.fromEntries(Object.entries(g.nodes).filter(([, n]) => !n.hidden));
          const idxs = connectionIndicesCrossedBySlash(g.connections, vis, pts);
          if (idxs.length) {
            const rm = new Set(idxs);
            setGraph((prev) => ({
              ...prev,
              connections: prev.connections.filter((_, i) => !rm.has(i)),
            }));
          }
        }
      }
      setDrag(null);
      setMoodboardDrag(null);
      setMoodboardResize(null);
      setPanning(null);
      setConnect(null);
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
    return () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
    };
  }, [drag, moodboardDrag, moodboardResize, connect, panning, pan, scale, graph, visibleGraph, resolveMoodboard]);

  // ── trackpad/mouse wheel:
  // two-finger scroll => pan canvas
  // pinch / Ctrl/Cmd+wheel => zoom canvas
  const onWheel = (e) => {
    e.preventDefault();
    const isZoomGesture = e.ctrlKey || e.metaKey;

    if (!isZoomGesture) {
      setPan((p) => ({ x: p.x - e.deltaX, y: p.y - e.deltaY }));
      return;
    }

    const factor = e.deltaY > 0 ? 0.92 : 1.08;
    const r = wrapRef.current.getBoundingClientRect();
    const cx = e.clientX - r.left, cy = e.clientY - r.top;
    const nextScale = Math.max(FIT_SCALE_MIN, Math.min(FIT_SCALE_MAX, scale * factor));
    setPan(p => ({ x: cx - (cx - p.x) * (nextScale/scale), y: cy - (cy - p.y) * (nextScale/scale) }));
    setScale(nextScale);
  };

  const resetView = () => {
    const el = wrapRef.current;
    if (route === 'studio' && el) {
      const { width, height } = el.getBoundingClientRect();
      if (width >= 48 && height >= 48) {
        const { pan: p, scale: s } = fitCanvasToView(width, height, graph.nodes, graph.moodboard);
        setPan(p);
        setScale(s);
        return;
      }
    }
    setPan({ x: 0, y: 0 });
    setScale(1);
  };
  return {
    wrapRef, scissorStroke, connect, panning, wireTool,
    onCanvasMouseDown, onWheel, onCanvasDragOver, onCanvasDrop,
    onHeaderMouseDown, onPortMouseDown, onPortMouseUp,
    updateNode, setNodeCollapsed, updateCraftParam, changeCraftType, changeProduct,
    deleteConnection, setWireToolSafe, duplicateNode, deleteNode, resetView,
    updateMoodboard, onMoodboardMoveStart, onMoodboardResizeStart,
  };
}
window.useCxEditorCanvas = useCxEditorCanvas;
