// Project Flow — free-form, draggable canvas (one diagram, compact + fullscreen).
// Drag boxes to reposition, drag from a box's handle to another box to link them,
// pan the empty canvas, zoom, and auto-tidy. Seeded from the timeline with a neat
// layered layout so it starts professional. Persists positions + links as JSON.
// window.ProjectFlow({ p, det, onChange })
(function () {
  const { useState, useRef, useEffect, useCallback } = React;

  const NW = 160, NH = 60;            // node box size (world units)
  const COL = 230, ROW = 140;         // seed spacing
  const X0 = 30, Y0 = 26;

  const TYPES = [
    { id: "page",     label: "เฟส / ขั้นตอน" },
    { id: "content",  label: "เหตุการณ์สำคัญ" },
    { id: "action",   label: "งานดำเนินการ" },
    { id: "decision", label: "ตัดสินใจ (Yes/No)" },
    { id: "negative", label: "ผลลบ / หยุด" },
    { id: "confirm",  label: "สำเร็จ / จบ" },
  ];
  let _eid = 0;
  const eid = () => "e" + (Date.now().toString(36)) + (_eid++);

  function detailOf(t) {
    return [
      t.assignee || t.owner ? "ผู้รับผิดชอบ: " + (t.assignee || t.owner) : "",
      t.item_date ? "วันที่: " + t.item_date + (t.item_date_end && t.item_date_end !== t.item_date ? " – " + t.item_date_end : "") : "",
    ].filter(Boolean).join("\n");
  }

  // ── Seed a tidy diagram from the timeline (phase rows, items along each row) ──
  function seedFromTimeline(det) {
    const nodes = [], edges = [];
    let prevPhase = null, lastTail = null;
    const phases = det.phases || [];
    phases.forEach((ph, li) => {
      const y = Y0 + li * ROW, pid = "ph-" + ph.id;
      nodes.push({ id: pid, type: "page", label: ph.name, detail: ph.en || "", x: X0, y });
      if (prevPhase) edges.push({ id: eid(), from: prevPhase, to: pid });
      let prev = pid, i = 0;
      (ph.tasks || []).slice().sort((a, b) => (a.s || 0) - (b.s || 0)).forEach((t) => {
        const isM = window.isMilestone ? window.isMilestone(t) : t.type === "milestone";
        i++;
        nodes.push({ id: "tk-" + t.id, type: isM ? "content" : "action", label: t.name, detail: detailOf(t), x: X0 + i * COL, y });
        edges.push({ id: eid(), from: prev, to: "tk-" + t.id });
        prev = "tk-" + t.id;
      });
      prevPhase = pid; lastTail = prev;
    });
    if (phases.length) {
      const y = Y0 + phases.length * ROW;
      nodes.push({ id: "confirm-end", type: "confirm", label: "เสร็จสมบูรณ์ ✓", detail: "", x: X0 + COL, y });
      if (lastTail) edges.push({ id: eid(), from: lastTail, to: "confirm-end" });
    }
    // dependencies
    phases.forEach((ph) => (ph.tasks || []).forEach((t) => (t.deps || []).forEach((dep) => {
      const from = "tk-" + dep.from, to = "tk-" + t.id;
      if (nodes.find((n) => n.id === from) && nodes.find((n) => n.id === to))
        edges.push({ id: eid(), from, to, label: dep.type === "soft" ? "ควร" : "ต้องก่อน" });
    })));
    return { v: 3, nodes, edges, seeded: true };
  }

  // ── Auto-tidy: layered (longest-path) layout assigns fresh x,y ──
  function tidyPositions(flow) {
    const nodes = flow.nodes || [], edges = flow.edges || [];
    const byId = {}; nodes.forEach((n) => (byId[n.id] = n));
    const out = {}, indeg = {};
    nodes.forEach((n) => { out[n.id] = []; indeg[n.id] = 0; });
    edges.forEach((e) => { if (byId[e.from] && byId[e.to]) { out[e.from].push(e.to); indeg[e.to]++; } });
    const rank = {}; nodes.forEach((n) => (rank[n.id] = 0));
    const deg = Object.assign({}, indeg);
    const q = nodes.filter((n) => deg[n.id] === 0).map((n) => n.id);
    const seen = new Set(q);
    while (q.length) {
      const u = q.shift();
      out[u].forEach((v) => { if (rank[v] < rank[u] + 1) rank[v] = rank[u] + 1; if (--deg[v] === 0 && !seen.has(v)) { seen.add(v); q.push(v); } });
    }
    const cols = {};
    nodes.forEach((n) => { (cols[rank[n.id]] = cols[rank[n.id]] || []).push(n); });
    const next = nodes.map((n) => ({ ...n }));
    const nById = {}; next.forEach((n) => (nById[n.id] = n));
    Object.keys(cols).map(Number).sort((a, b) => a - b).forEach((r) => {
      cols[r].forEach((n, i) => { nById[n.id].x = X0 + r * COL; nById[n.id].y = Y0 + i * ROW; });
    });
    return { ...flow, nodes: next };
  }

  // Border intersection point of a node box toward (tx,ty) — used for the
  // temporary "drag-to-link" line.
  function border(n, tx, ty) {
    const cx = n.x + NW / 2, cy = n.y + NH / 2, dx = tx - cx, dy = ty - cy;
    if (!dx && !dy) return { x: cx, y: cy };
    const s = Math.min(dx ? (NW / 2) / Math.abs(dx) : Infinity, dy ? (NH / 2) / Math.abs(dy) : Infinity);
    return { x: cx + dx * s, y: cy + dy * s };
  }

  // Orthogonal (right-angle) connector between two boxes — tidy, straight
  // segments. Exits the side facing the target and jogs at the midpoint.
  function orthEdge(a, b) {
    const acx = a.x + NW / 2, acy = a.y + NH / 2, bcx = b.x + NW / 2, bcy = b.y + NH / 2;
    const dx = bcx - acx, dy = bcy - acy;
    if (Math.abs(dx) >= Math.abs(dy)) {                 // horizontal dominant
      const sx = dx >= 0 ? a.x + NW : a.x, tx = dx >= 0 ? b.x : b.x + NW;
      const mx = (sx + tx) / 2;
      return { d: `M ${sx},${acy} H ${mx} V ${bcy} H ${tx}`, lx: mx, ly: (acy + bcy) / 2 };
    }
    const sy = dy >= 0 ? a.y + NH : a.y, ty = dy >= 0 ? b.y : b.y + NH;   // vertical dominant
    const my = (sy + ty) / 2;
    return { d: `M ${acx},${sy} V ${my} H ${bcx} V ${ty}`, lx: (acx + bcx) / 2, ly: my };
  }

  window.ProjectFlow = function ProjectFlow({ p, det, onChange }) {
    const canEdit = window.can ? window.can("edit") : false;
    const stored = det && det.flow && det.flow.v === 3 && (det.flow.nodes || []).length ? det.flow : null;
    const [flow, setFlow] = useState(() => stored || seedFromTimeline(det));
    const [view, setView] = useState({ tx: 0, ty: 0, scale: 1 });
    const [full, setFull] = useState(false);
    const [edit, setEdit] = useState(null);
    const [linking, setLinking] = useState(null);     // {from, x, y} while dragging a new arrow
    const [linkTarget, setLinkTarget] = useState(null); // node id highlighted as the drop target
    const flowRef = useRef(flow), viewRef = useRef(view), vpRef = useRef(null);
    flowRef.current = flow; viewRef.current = view;

    const persist = useCallback((next) => {
      setFlow(next);
      if (window.DB && window.DB.saveFlow) window.DB.saveFlow(p.id, next).then(() => onChange && onChange());
    }, [p.id, onChange]);
    const commit = useCallback(() => {
      const f = flowRef.current;
      if (window.DB && window.DB.saveFlow) window.DB.saveFlow(p.id, f).then(() => onChange && onChange());
    }, [p.id, onChange]);

    function screenToWorld(cx, cy) {
      const r = vpRef.current.getBoundingClientRect(), v = viewRef.current;
      return { x: (cx - r.left - v.tx) / v.scale, y: (cy - r.top - v.ty) / v.scale };
    }
    function hitNode(wx, wy) {
      const ns = flowRef.current.nodes;
      for (let i = ns.length - 1; i >= 0; i--) { const n = ns[i]; if (wx >= n.x && wx <= n.x + NW && wy >= n.y && wy <= n.y + NH) return n.id; }
      return null;
    }

    // drag a node (or click to edit if it didn't move)
    function onNodeDown(e, n) {
      if (!canEdit) { setEdit({ ...n }); return; }
      e.stopPropagation();
      const sx = e.clientX, sy = e.clientY, ox = n.x, oy = n.y; let moved = false;
      const move = (ev) => {
        const sc = viewRef.current.scale;
        if (Math.abs(ev.clientX - sx) + Math.abs(ev.clientY - sy) > 4) moved = true;
        setFlow((f) => ({ ...f, nodes: f.nodes.map((m) => m.id === n.id ? { ...m, x: ox + (ev.clientX - sx) / sc, y: oy + (ev.clientY - sy) / sc } : m) }));
      };
      const up = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); if (moved) commit(); else setEdit({ ...n }); };
      window.addEventListener("pointermove", move); window.addEventListener("pointerup", up);
    }

    // drag from a node's handle to create a link
    function onHandleDown(e, n) {
      e.stopPropagation(); e.preventDefault();
      const w0 = screenToWorld(e.clientX, e.clientY);
      setLinking({ from: n.id, x: w0.x, y: w0.y });
      const move = (ev) => {
        const w = screenToWorld(ev.clientX, ev.clientY);
        setLinking((l) => l && { ...l, x: w.x, y: w.y });
        const t = hitNode(w.x, w.y);
        setLinkTarget(t && t !== n.id ? t : null);
      };
      const up = (ev) => {
        window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up);
        const w = screenToWorld(ev.clientX, ev.clientY), to = hitNode(w.x, w.y);
        setLinking(null); setLinkTarget(null);
        if (to && to !== n.id) {
          const f = flowRef.current;
          if (!f.edges.some((ed) => ed.from === n.id && ed.to === to)) persist({ ...f, edges: f.edges.concat([{ id: eid(), from: n.id, to }]) });
        }
      };
      window.addEventListener("pointermove", move); window.addEventListener("pointerup", up);
    }

    // pan the canvas background
    function onCanvasDown(e) {
      if (e.button !== 0) return;
      const sx = e.clientX, sy = e.clientY, otx = viewRef.current.tx, oty = viewRef.current.ty;
      const move = (ev) => setView((v) => ({ ...v, tx: otx + (ev.clientX - sx), ty: oty + (ev.clientY - sy) }));
      const up = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
      window.addEventListener("pointermove", move); window.addEventListener("pointerup", up);
    }

    function zoom(f) { setView((v) => ({ ...v, scale: Math.max(0.3, Math.min(2, v.scale * f)) })); }
    function fit() {
      const ns = flowRef.current.nodes; if (!ns.length) return;
      const r = vpRef.current.getBoundingClientRect();
      let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
      ns.forEach((n) => { minX = Math.min(minX, n.x); minY = Math.min(minY, n.y); maxX = Math.max(maxX, n.x + NW); maxY = Math.max(maxY, n.y + NH); });
      const w = maxX - minX + 80, h = maxY - minY + 80;
      const scale = Math.max(0.3, Math.min(1.4, Math.min(r.width / w, r.height / h)));
      setView({ scale, tx: (r.width - w * scale) / 2 - (minX - 40) * scale, ty: (r.height - h * scale) / 2 - (minY - 40) * scale });
    }
    function onWheel(e) { if (e.ctrlKey || e.metaKey) { e.preventDefault(); zoom(e.deltaY < 0 ? 1.1 : 0.9); } }

    function saveNode(data) {
      const f = flowRef.current, nodes = f.nodes.slice();
      if (!data.id) {
        const w = vpRef.current.getBoundingClientRect(), c = screenToWorld(w.left + w.width / 2, w.top + w.height / 2);
        nodes.push({ id: "n-" + Date.now().toString(36), type: data.type, label: data.label, detail: data.detail, x: c.x - NW / 2, y: c.y - NH / 2 });
      } else {
        const i = nodes.findIndex((n) => n.id === data.id);
        if (i >= 0) nodes[i] = { ...nodes[i], type: data.type, label: data.label, detail: data.detail };
      }
      persist({ ...f, nodes }); setEdit(null);
    }
    function deleteNode(id) {
      if (!confirm("ลบกล่องนี้และเส้นที่เกี่ยวข้อง?")) return;
      const f = flowRef.current;
      persist({ ...f, nodes: f.nodes.filter((n) => n.id !== id), edges: f.edges.filter((e) => e.from !== id && e.to !== id) });
      setEdit(null);
    }
    function deleteEdge(id) { const f = flowRef.current; persist({ ...f, edges: f.edges.filter((e) => e.id !== id) }); }

    const nodes = flow.nodes || [];
    const byId = {}; nodes.forEach((n) => (byId[n.id] = n));
    const edges = (flow.edges || []).filter((e) => byId[e.from] && byId[e.to]);
    let worldW = 600, worldH = 400;
    nodes.forEach((n) => { worldW = Math.max(worldW, n.x + NW + 120); worldH = Math.max(worldH, n.y + NH + 120); });

    const canvas = (
      <div className="flow-viewport" ref={vpRef} onPointerDown={onCanvasDown} onWheel={onWheel}>
        <div className="flow-world" style={{ transform: `translate(${view.tx}px,${view.ty}px) scale(${view.scale})`, width: worldW, height: worldH }}>
          <svg className="flow-svg" width={worldW} height={worldH}>
            <defs>
              <marker id="fa3" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
                <path d="M0,0 L6,3 L0,6 Z" fill="var(--ink2)" />
              </marker>
            </defs>
            {edges.map((e) => {
              const a = byId[e.from], b = byId[e.to];
              const { d, lx, ly } = orthEdge(a, b);
              return (
                <g key={e.id} className="fl-edge">
                  <path className="fl-edge-hit" d={d} onClick={canEdit ? () => { if (confirm("ลบเส้นเชื่อมนี้?")) deleteEdge(e.id); } : undefined} />
                  <path className="fl-edge-line" d={d} markerEnd="url(#fa3)" />
                  {e.label ? <text className="fl-elabel" x={lx} y={ly - 5} textAnchor="middle">{e.label}</text> : null}
                </g>
              );
            })}
            {linking && byId[linking.from] ? (() => {
              const a = byId[linking.from], s = border(a, linking.x, linking.y);
              return <path className="fl-edge-temp" d={`M ${s.x},${s.y} L ${linking.x},${linking.y}`} markerEnd="url(#fa3)" />;
            })() : null}
          </svg>
          {nodes.map((n) => (
            <div key={n.id} className={"fl-node fl-" + n.type + (canEdit ? " editable" : "") + (linkTarget === n.id ? " link-target" : "") + (linking && linking.from === n.id ? " link-source" : "")}
                 style={{ left: n.x, top: n.y, width: NW, height: NH }}
                 title={(n.label || "") + (n.detail ? "\n\n" + n.detail : "")}
                 onPointerDown={(e) => onNodeDown(e, n)}>
              <span className="fl-node-txt">{n.label || "(ไม่มีชื่อ)"}</span>
              {canEdit && <span className="fl-handle" title="ลากไปยังกล่องอื่นเพื่อเชื่อมลูกศร" onPointerDown={(e) => onHandleDown(e, n)}>+</span>}
            </div>
          ))}
        </div>
      </div>
    );

    const toolbar = (
      <div className="flow-toolbar">
        <div>
          <div className="sec-title">แผนผังภาพรวมการทำงาน</div>
          <div style={{ fontSize: 12, color: "var(--muted)" }}>{nodes.length} กล่อง · {edges.length} เส้นเชื่อม{canEdit ? " · ลากกล่องเพื่อย้าย · ลากจุดมุมเพื่อโยงเส้น" : ""}</div>
        </div>
        <div className="spacer" />
        <div className="flow-zoom">
          <button className="btn sm" onClick={() => zoom(0.9)} title="ย่อ">−</button>
          <button className="btn sm" onClick={fit} title="พอดีหน้าจอ">⤢</button>
          <button className="btn sm" onClick={() => zoom(1.1)} title="ขยาย">+</button>
        </div>
        {canEdit && <button className="btn sm" onClick={() => persist(tidyPositions(flow))} title="จัดกล่องให้เป็นระเบียบอัตโนมัติ">⌗ จัดระเบียบ</button>}
        {canEdit && <button className="btn sm" onClick={() => { if (confirm("สร้างใหม่จากไทม์ไลน์? การจัดวาง/แก้ไขจะถูกแทนที่")) persist(seedFromTimeline(det)); }}>↻ สร้างใหม่</button>}
        {canEdit && <button className="btn sm pri" onClick={() => setEdit({ type: "action", label: "", detail: "" })}>+ เพิ่มกล่อง</button>}
        <button className="btn sm" onClick={() => setFull((v) => !v)} title={full ? "ออกจากเต็มจอ" : "เต็มจอ"}>{full ? "✕ ออก" : "⛶ เต็มจอ"}</button>
      </div>
    );

    const legend = (
      <div className="flow-legend">
        {TYPES.map((t) => <span key={t.id} className="fl-lg"><span className={"fl-chip fl-" + t.id} />{t.label}</span>)}
      </div>
    );

    const editModal = edit && <FlowNodeModal node={edit} onSave={saveNode} onDelete={deleteNode} onClose={() => setEdit(null)} />;

    if (full) {
      return ReactDOM.createPortal(
        <div className="flow-full">
          {toolbar}{legend}
          <div className="flow-full-canvas">{canvas}</div>
          {editModal}
        </div>, document.body);
    }
    return (
      <div className="flow-tab">
        {toolbar}{legend}
        {nodes.length === 0
          ? <div className="flow-empty">ยังไม่มีเฟส/งานในไทม์ไลน์ — เพิ่มในแท็บ “Edit Timeline” แล้วกด “สร้างใหม่”</div>
          : <div className="flow-compact-canvas">{canvas}</div>}
        {editModal}
      </div>
    );
  };

  // ── Add / edit node modal ──────────────────────────────────────
  function FlowNodeModal({ node, onSave, onDelete, onClose }) {
    const [label, setLabel] = useState(node.label || "");
    const [type, setType] = useState(node.type || "action");
    const [detail, setDetail] = useState(node.detail || "");
    return (
      <div className="rt-overlay" onClick={onClose}>
        <div className="rt-modal" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 480 }}>
          <div className="rt-head">
            <h2 className="rt-title">{node.id ? "แก้ไขกล่อง" : "เพิ่มกล่องใหม่"}</h2>
            <button type="button" className="icon-btn rt-x" onClick={onClose} title="ปิด">{window.IC ? window.IC.x : "×"}</button>
          </div>
          <div className="rt-body">
            <label className="fl-field"><span>ชื่อกล่อง</span>
              <input className="fl-input" value={label} onChange={(e) => setLabel(e.target.value)} placeholder="เช่น ขออนุมัติงบประมาณ" autoFocus />
            </label>
            <label className="fl-field"><span>ชนิดกล่อง</span>
              <select className="fl-input" value={type} onChange={(e) => setType(e.target.value)}>
                {TYPES.map((t) => <option key={t.id} value={t.id}>{t.label}</option>)}
              </select>
            </label>
            <label className="fl-field"><span>รายละเอียด (ไม่บังคับ)</span>
              <textarea className="fl-input" rows={3} value={detail} onChange={(e) => setDetail(e.target.value)} placeholder="ข้อมูลเพิ่มเติม แสดงตอนชี้เมาส์" />
            </label>
          </div>
          <div className="fl-modal-foot">
            {node.id ? <button className="btn sm danger-ghost" onClick={() => onDelete(node.id)}>ลบกล่อง</button> : <span />}
            <div style={{ display: "flex", gap: 8 }}>
              <button className="btn sm" onClick={onClose}>ยกเลิก</button>
              <button className="btn sm pri" disabled={!label.trim()} onClick={() => onSave({ id: node.id, type, label: label.trim(), detail })}>บันทึก</button>
            </div>
          </div>
        </div>
      </div>
    );
  }
})();
