// Main App
function App(){
  const initial = {
    title: '',
    subtitle: '',
    showTitleOnCanvas: false,
    bgPattern: 'bg-dots',
    bgColor: '#FFF7EC',
    nodes: [],
    edges: [],
    groups: [],
    writings: [],
  };

  const hist = useHistory(initial);
  const { state, set } = hist;
  const [selection, setSelection] = React.useState(null);
  const [view, setView] = React.useState({ zoom: 1, tx: 200, ty: 120 });
  const [editingEdgeId, setEditingEdgeId] = React.useState(null);
  const [toast, setToast] = React.useState(null);
  const [shotMode, setShotMode] = React.useState(false);
  // Group compose mode: when non-null, clicking a node toggles its membership in that group.
  const [composingGroupId, setComposingGroupId] = React.useState(null);
  const shotPrevViewRef = React.useRef(null);
  const canvasRef = React.useRef(null);

  // ---- toast helper
  const flash = (msg)=>{ setToast(msg); setTimeout(()=>setToast(null), 1800); };

  // ---- mutations
  const nextId = (prefix) => prefix + '_' + Math.random().toString(36).slice(2,8);

  const addNode = (partial={}) => {
    // Place near center of current view
    const stage = canvasRef.current;
    const rect = stage?.getBoundingClientRect();
    const cx = rect ? (rect.width/2 - view.tx)/view.zoom : 400;
    const cy = rect ? (rect.height/2 - view.ty)/view.zoom : 300;
    const color = NODE_COLORS[state.nodes.length % NODE_COLORS.length];
    const id = nextId('n');
    set(s => ({
      ...s,
      nodes:[...s.nodes, {
        id, x: cx + (Math.random()-0.5)*80, y: cy + (Math.random()-0.5)*80,
        name:'New character', color, emoji: null, image: null, shape:'circle',
        ...partial,
      }]
    }));
    setSelection({type:'node', id});
  };

  const updateNode = (id, patch, opts) => {
    set(s => ({
      ...s,
      nodes: s.nodes.map(n => n.id===id ? { ...n, ...patch } : n),
    }), opts);
  };

  const deleteNode = (id) => {
    set(s => {
      // Remove node from any groups; auto-drop groups that become empty.
      const groups = (s.groups || [])
        .map(g => ({ ...g, nodeIds: (g.nodeIds || []).filter(nid => nid !== id) }))
        .filter(g => (g.nodeIds || []).length > 0);
      // Drop any writing-link that points to this node — orphaned edges to it
      // also disappear below, so writing-links to those edges drop too.
      const removedEdgeIds = new Set(s.edges.filter(e=>e.from===id||e.to===id).map(e=>e.id));
      const writings = (s.writings || []).map(w => {
        const links = (w.links || []).filter(l =>
          !(l.type === 'node' && l.id === id) &&
          !(l.type === 'edge' && removedEdgeIds.has(l.id))
        );
        return links.length === (w.links || []).length ? w : { ...w, links };
      });
      return {
        ...s,
        nodes: s.nodes.filter(n=>n.id!==id),
        edges: s.edges.filter(e=>e.from!==id && e.to!==id),
        groups,
        writings,
      };
    });
    setSelection(null);
  };

  const addEdge = (fromId, toId, partial={}) => {
    const id = nextId('e');
    set(s => ({
      ...s,
      edges:[...s.edges, {
        id, from:fromId, to:toId,
        relation:'like', style:'oneway',
        color: RELATIONS[0].color, label: null, stamp: '♡',
        ...partial,
      }]
    }));
    setSelection({type:'edge', id});
  };

  const updateEdge = (id, patch, opts) => {
    set(s => ({
      ...s,
      edges: s.edges.map(e => e.id===id ? { ...e, ...patch } : e),
    }), opts);
  };

  const deleteEdge = (id) => {
    set(s => ({
      ...s,
      edges: s.edges.filter(e=>e.id!==id),
      writings: (s.writings || []).map(w => {
        const links = (w.links || []).filter(l => !(l.type === 'edge' && l.id === id));
        return links.length === (w.links || []).length ? w : { ...w, links };
      }),
    }));
    setSelection(null);
  };

  // -------- Writings (free-floating annotation text on top of everything) --------
  const WRITING_DEFAULT_COLOR = '#F56A92';
  const addWriting = (partial = {}) => {
    const stage = canvasRef.current;
    const rect = stage?.getBoundingClientRect();
    const cx = rect ? (rect.width/2 - view.tx)/view.zoom : 400;
    const cy = rect ? (rect.height/2 - view.ty)/view.zoom : 300;
    const id = nextId('w');
    set(s => ({
      ...s,
      writings: [...(s.writings || []), {
        id,
        x: cx + (Math.random()-0.5)*60,
        y: cy + (Math.random()-0.5)*60,
        text: 'Hello',
        color: WRITING_DEFAULT_COLOR,
        fontSize: 28,
        rotation: 0,
        // links: each entry { type:'node'|'edge', id, lineCurve } — a writing
        // can have any number of links. Empty array = free-floating.
        links: [],
        ...partial,
      }],
    }));
    setSelection({ type: 'writing', id });
  };

  const updateWriting = (id, patch, opts) => {
    set(s => ({
      ...s,
      writings: (s.writings || []).map(w => w.id === id ? { ...w, ...patch } : w),
    }), opts);
  };

  const deleteWriting = (id) => {
    set(s => ({
      ...s,
      writings: (s.writings || []).filter(w => w.id !== id),
    }));
    setSelection(null);
  };

  // -------- Groups (cluster of nodes with a blob visual) --------
  const GROUP_COLORS = ['#FFB3C7','#FFD3A8','#FFE8A3','#C7EFC0','#B5D9F2','#C9BEF5','#E8BCE8'];

  const createGroupFromNode = (nodeId) => {
    const id = nextId('g');
    const usedColors = (state.groups || []).map(g=>g.color);
    const color = GROUP_COLORS.find(c => !usedColors.includes(c)) || GROUP_COLORS[0];
    set(s => ({
      ...s,
      groups: [...(s.groups || []), { id, name:'', color, showName:true, nodeIds:[nodeId] }],
    }));
    return id;
  };

  const updateGroup = (id, patch, opts) => {
    set(s => ({
      ...s,
      groups: (s.groups || []).map(g => g.id === id ? { ...g, ...patch } : g),
    }), opts);
  };

  const deleteGroup = (id) => {
    set(s => ({
      ...s,
      groups: (s.groups || []).filter(g => g.id !== id),
    }));
    setSelection(null);
    if (composingGroupId === id) setComposingGroupId(null);
  };

  // Toggle a node's membership in a group during compose mode.
  // Multi-membership is allowed — a node can belong to any number of groups.
  const toggleGroupMember = (groupId, nodeId) => {
    set(s => {
      const groups = (s.groups || []).map(g => {
        if (g.id !== groupId) return g;
        const has = (g.nodeIds || []).includes(nodeId);
        const nodeIds = has
          ? g.nodeIds.filter(x => x !== nodeId)
          : [...(g.nodeIds || []), nodeId];
        return { ...g, nodeIds };
      })
      // drop any group that became empty (except the one we're composing)
      .filter(g => g.id === groupId || (g.nodeIds || []).length > 0);
      return { ...s, groups };
    });
  };

  // Swap two edges' positions in state.edges — later edges render on top,
  // so this effectively swaps their z-order. Triggered by Shift+click.
  const swapEdgeZ = (id1, id2) => {
    set(s => {
      const i1 = s.edges.findIndex(e => e.id === id1);
      const i2 = s.edges.findIndex(e => e.id === id2);
      if (i1 < 0 || i2 < 0 || i1 === i2) return s;
      const edges = [...s.edges];
      [edges[i1], edges[i2]] = [edges[i2], edges[i1]];
      return {...s, edges};
    });
  };

  const clearAll = () => {
    if (!confirm('Delete all characters and relationships?')) return;
    set(s => ({...s, nodes:[], edges:[], groups:[], writings:[], title:'', subtitle:'', showTitleOnCanvas:false, bgPattern:'bg-dots', bgColor:'#FFF7EC'}));
    setSelection(null);
    setComposingGroupId(null);
    try {
      localStorage.removeItem('sankaku_saved_data');
    } catch(e) {}
  };

  // ---- demo data
  const loadDemo = () => {
    const demo = buildDemoData();
    set(s => ({
      ...s,
      nodes: demo.nodes, edges: demo.edges, groups: demo.groups || [],
      title: 'Sunnyvale Academy Relationships', subtitle: 'Season 1 Main Cast',
      showTitleOnCanvas: true,
    }));
    setView({ zoom: 0.85, tx: 100, ty: 40 });
    setSelection(null);
    flash('Sample loaded ✿');
  };

  // ---- setters bound to state via set()
  const setTitle = (t)=> set(s=>({...s,title:t}));
  const setSubtitle = (t)=> set(s=>({...s,subtitle:t}));
  const setShowTitleOnCanvas = (v)=> set(s=>({...s,showTitleOnCanvas:v}));
  const setBgPattern = (v)=> set(s=>({...s,bgPattern:v}));
  const setBgColor = (v)=> set(s=>({...s,bgColor:v}));

  // ---- project file save / load
  const saveProject = () => {
    const data = JSON.stringify(state, null, 2);
    const blob = new Blob([data], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = (state.title || 'relationship-diagram') + '.sankaku.json';
    a.click();
    URL.revokeObjectURL(url);
    flash('Project file saved 💾');
  };

  const loadProjectFileRef = React.useRef(null);
  const loadProject = () => {
    loadProjectFileRef.current?.click();
  };
  const onLoadProjectFile = (e) => {
    const f = e.target.files?.[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const data = JSON.parse(reader.result);
        if (!confirm('Your current work will be overwritten. Continue?')) return;
        set(s => ({
          ...s,
          title: data.title ?? '',
          subtitle: data.subtitle ?? '',
          showTitleOnCanvas: data.showTitleOnCanvas ?? false,
          bgPattern: data.bgPattern ?? 'bg-dots',
          bgColor: data.bgColor ?? '#FFF7EC',
          nodes: data.nodes ?? [],
          edges: data.edges ?? [],
          groups: data.groups ?? [],
          // Migrate old single-link writings (attachedTo + lineCurve) to the
          // new links[] shape so existing project files keep working.
          writings: (data.writings ?? []).map(w => {
            if (Array.isArray(w.links)) return w;
            if (w.attachedTo) {
              return { ...w, links: [{ type: w.attachedTo.type, id: w.attachedTo.id, lineCurve: w.lineCurve || 0 }] };
            }
            return { ...w, links: [] };
          }),
        }));
        setSelection(null);
        setComposingGroupId(null);
        setView({ zoom: 1, tx: 200, ty: 120 });
        flash('Project file loaded 📂');
      } catch(err) {
        flash('Failed to load file');
        console.error(err);
      }
    };
    reader.readAsText(f);
    e.target.value = '';
  };

  // ---- screenshot mode
  const enterShotMode = () => {
    if (state.nodes.length === 0) { flash('Add a character first'); return; }
    setSelection(null);
    const pad = 80;
    let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
    state.nodes.forEach(n=>{
      minX = Math.min(minX, n.x-70); minY = Math.min(minY, n.y-70);
      maxX = Math.max(maxX, n.x+70); maxY = Math.max(maxY, n.y+70);
    });
    if (state.showTitleOnCanvas) minY = Math.min(minY, 0);
    minX-=pad; minY-=pad; maxX+=pad; maxY+=pad;
    const bw = maxX-minX, bh = maxY-minY;
    const vw = window.innerWidth, vh = window.innerHeight;
    const zoom = Math.min(1.5, Math.max(0.3, Math.min(vw/bw, vh/bh)));
    const tx = (vw - bw*zoom)/2 - minX*zoom;
    const ty = (vh - bh*zoom)/2 - minY*zoom;
    shotPrevViewRef.current = view;
    setView({ zoom, tx, ty });
    setShotMode(true);
    document.body.classList.add('is-shot-mode');
  };
  const exitShotMode = () => {
    setShotMode(false);
    document.body.classList.remove('is-shot-mode');
    if (shotPrevViewRef.current) {
      setView(shotPrevViewRef.current);
      shotPrevViewRef.current = null;
    }
  };

  // ---- keyboard
  React.useEffect(()=>{
    const onKey = (e) => {
      if (shotMode && e.key==='Escape') { exitShotMode(); return; }
      if (e.key==='Escape' && composingGroupId) { setComposingGroupId(null); return; }
      if (e.target.matches('input,textarea')) return;
      if ((e.metaKey||e.ctrlKey) && e.key==='z' && !e.shiftKey){ e.preventDefault(); hist.undo(); }
      else if ((e.metaKey||e.ctrlKey) && (e.key==='y' || (e.shiftKey && e.key==='z'))){ e.preventDefault(); hist.redo(); }
      else if (e.key==='Delete' || e.key==='Backspace'){
        if (selection?.type==='node') deleteNode(selection.id);
        if (selection?.type==='edge') deleteEdge(selection.id);
        if (selection?.type==='group') deleteGroup(selection.id);
        if (selection?.type==='writing') deleteWriting(selection.id);
      }
    };
    window.addEventListener('keydown', onKey);
    return ()=> window.removeEventListener('keydown', onKey);
  }, [selection, hist, shotMode, composingGroupId]);

  // ---- edge label inline editor
  const editingEdge = state.edges.find(e=>e.id===editingEdgeId);

  return (
    <div className="app">
      <Toolbar
        title={state.title} setTitle={setTitle}
        subtitle={state.subtitle} setSubtitle={setSubtitle}
        showTitleOnCanvas={state.showTitleOnCanvas} setShowTitleOnCanvas={setShowTitleOnCanvas}
        bgPattern={state.bgPattern} setBgPattern={setBgPattern}
        bgColor={state.bgColor} setBgColor={setBgColor}
        canUndo={hist.canUndo} canRedo={hist.canRedo}
        onUndo={hist.undo} onRedo={hist.redo}
        onAddNode={()=>addNode()}
        onAddWriting={()=>addWriting()}
        onShotMode={enterShotMode}
        onSaveProject={saveProject}
        onLoadProject={loadProject}
        onLoadDemo={loadDemo}
        onClear={clearAll}
      />
      <input ref={loadProjectFileRef} className="file-hidden" type="file" accept=".json,.sankaku.json" onChange={onLoadProjectFile}/>

      <div className="canvas-wrap">
        <Canvas
          nodes={state.nodes} edges={state.edges} groups={state.groups || []}
          writings={state.writings || []}
          selection={selection} setSelection={setSelection}
          updateNode={updateNode} deleteNode={deleteNode} addEdge={addEdge} updateEdge={updateEdge}
          updateWriting={updateWriting} deleteWriting={deleteWriting}
          swapEdgeZ={swapEdgeZ} flash={flash}
          composingGroupId={composingGroupId} setComposingGroupId={setComposingGroupId}
          toggleGroupMember={toggleGroupMember}
          updateGroup={updateGroup}
          view={view} setView={setView} canvasRef={canvasRef}
          bgPattern={state.bgPattern} bgColor={state.bgColor}
          title={state.title} subtitle={state.subtitle}
          showTitleOnCanvas={state.showTitleOnCanvas}
          onEditEdge={(id)=>setEditingEdgeId(id)}
        />
        {state.nodes.length === 0 && (
          <div className="welcome">
            <div className="card">
              <div style={{fontSize:48}}>💕</div>
              <h1>Triangle Relationship Generator</h1>
              <p>Add characters, then draw arrows for<br/>love, dislike, rivalry and more.</p>
              <div className="row">
                <button className="btn primary" onClick={()=>addNode()}>＋ Add first node</button>
                <button className="btn" onClick={loadDemo}>🎀 See a sample</button>
              </div>
            </div>
          </div>
        )}
      </div>

      <Inspector
        selection={selection} setSelection={setSelection}
        nodes={state.nodes} edges={state.edges} groups={state.groups || []}
        writings={state.writings || []}
        updateNode={updateNode} updateEdge={updateEdge}
        updateWriting={updateWriting} deleteWriting={deleteWriting}
        deleteNode={deleteNode} deleteEdge={deleteEdge}
        addNode={()=>addNode()} addWriting={()=>addWriting()} clearAll={clearAll}
        onLoadDemo={loadDemo}
        createGroupFromNode={createGroupFromNode}
        updateGroup={updateGroup} deleteGroup={deleteGroup}
        composingGroupId={composingGroupId} setComposingGroupId={setComposingGroupId}
      />

      {toast && <div className="toast">{toast}</div>}

      {shotMode && (
        <>
          <button className="shot-exit" onClick={exitShotMode} title="Exit screenshot mode">✕</button>
          <div className="shot-hint" key={Date.now()}>
            📸 Take a screenshot —
            {navigator.platform?.includes('Mac')
              ? <><kbd>⌘</kbd>+<kbd>⇧</kbd>+<kbd>4</kbd></>
              : <><kbd>Win</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd></>}
          </div>
        </>
      )}
    </div>
  );
}

// ---------- demo data builder ----------
function buildDemoData(){
  const nodes = [
    // Hina is dead-center with many incoming arrows; pull her label UP so the
    // name pill doesn't fight with the arrow midpoints arriving from below.
    { id:'nA', name:'Hina',   subtitle:'Protagonist',      emoji:'🌸', color:'#FFB3C7', shape:'circle', x:600, y:260, labelPos:'top' },
    // Kaito sits at the left edge; pushing the label to the LEFT keeps it
    // clear of the e1/e5 arrow congestion just to his right.
    { id:'nB', name:'Kaito',  subtitle:'Childhood friend', emoji:'🐱', color:'#B5D9F2', shape:'circle', x:320, y:260, labelPos:'left' },
    { id:'nC', name:'Ren',    subtitle:'Student council',  emoji:'⭐', color:'#C9BEF5', shape:'square', x:880, y:260 },
    { id:'nD', name:'Mio',    subtitle:'Rival',            emoji:'🔥', color:'#FFD3A8', shape:'circle', x:600, y:500 },
    { id:'nE', name:'Ao',     subtitle:'Senior',           emoji:'🍵', color:'#C7EFC0', shape:'bubble', x:320, y:500 },
    { id:'nF', name:'Sora',   subtitle:'Junior',           emoji:'🐶', color:'#FFE8A3', shape:'circle', x:880, y:500 },
    { id:'nG', name:'Yui',    subtitle:'Best friend',      emoji:'🎀', color:'#E8BCE8', shape:'circle', x:150, y:100 },
  ];
  // e4 (Love, nC→nD) and e6 (Admires, nF→nA) cross right around (740, 380),
  // so their labels sat on top of each other. Bend them in opposite
  // perpendicular directions via customOffset to pull the labels apart — also
  // doubles as a showcase of the curve-adjustment feature.
  const edges = [
    { id:'e1', from:'nB', to:'nA', relation:'like',    style:'heart',   color:'#F56A92', label:'Love',        stamp:'♡' },
    { id:'e2', from:'nA', to:'nC', relation:'crush',   style:'oneway',  color:'#B7A0EE', label:'Crush',       stamp:'?' },
    { id:'e3', from:'nD', to:'nA', relation:'rival',   style:'cracked', color:'#F77F6E', label:'Rival',       stamp:'⚡' },
    { id:'e4', from:'nC', to:'nD', relation:'like',    style:'oneway',  color:'#F56A92', label:'Love',        stamp:'♡', customOffset:70 },
    { id:'e5', from:'nE', to:'nB', relation:'trust',   style:'twoway',  color:'#7DD8B4', label:'Trust',       stamp:'★' },
    { id:'e6', from:'nF', to:'nA', relation:'like',    style:'dotted',  color:'#F56A92', label:'Admires',     stamp:'♡', customOffset:-70 },
    { id:'e7', from:'nD', to:'nE', relation:'dislike', style:'wavy',    color:'#7a6a7a', label:'Dislike',     stamp:'✕' },
    { id:'e8', from:'nG', to:'nA', relation:'trust',   style:'twoway',  color:'#7DD8B4', label:'Best friend', stamp:'★' },
  ];
  // Two zones to showcase the group / blob feature: the heroine's circle of
  // trust on one side, the rivals on the other. Multi-membership is allowed
  // (e.g. Hina is naturally the hub of the friend group).
  // labelOffset nudges the group's name pill from its default (top-of-bbox)
  // anchor so labels sit in the most readable spot for THIS demo:
  //   g1 ↓ 100px so the pill lands just above the "Best friend" arrow label
  //   g2 ↑↑ from default into open space to the upper-right of "Admires"
  const groups = [
    { id:'g1', name:'Heroine\'s circle', color:'#FFB3C7', nodeIds:['nA','nB','nG','nE'], showName:true, labelOffset:{ dx: 0, dy: 80 } },
    { id:'g2', name:'Rivals',            color:'#F77F6E', nodeIds:['nC','nD'],           showName:true, labelOffset:{ dx: 70, dy: 180 } },
  ];
  return { nodes, edges, groups };
}

window.App = App;
window.buildDemoData = buildDemoData;
