// Board screen: kanban with optional swimlanes, filters, faint column colors, drag & drop.

const DragCtx = React.createContext(null);
function DragProvider({ children }) {
  const [drag, setDrag] = React.useState(null);
  return <DragCtx.Provider value={{ drag, setDrag }}>{children}</DragCtx.Provider>;
}

// Native HTML5 drag-and-drop suppresses mouse-wheel scrolling on most
// platforms while a drag is in progress (a browser limitation, not
// something a dragover/wheel handler can override). The standard
// workaround — used by Trello and friends — is edge autoscroll: while
// holding a card, ride it near the top/bottom/left/right edge of the
// scrollable board and the board scrolls on its own.
function useEdgeAutoscroll(wrapRef, active) {
  const posRef = React.useRef(null);
  React.useEffect(() => {
    if (!active) { posRef.current = null; return; }
    let raf;
    const EDGE = 70, MAX_SPEED = 22;
    function tick() {
      const el = wrapRef.current;
      const pos = posRef.current;
      if (el && pos) {
        const rect = el.getBoundingClientRect();
        let dy = 0, dx = 0;
        if (pos.y < rect.top + EDGE) dy = -MAX_SPEED * (1 - Math.max(0, pos.y - rect.top) / EDGE);
        else if (pos.y > rect.bottom - EDGE) dy = MAX_SPEED * (1 - Math.max(0, rect.bottom - pos.y) / EDGE);
        if (pos.x < rect.left + EDGE) dx = -MAX_SPEED * (1 - Math.max(0, pos.x - rect.left) / EDGE);
        else if (pos.x > rect.right - EDGE) dx = MAX_SPEED * (1 - Math.max(0, rect.right - pos.x) / EDGE);
        if (dx || dy) el.scrollBy(dx, dy);
      }
      raf = requestAnimationFrame(tick);
    }
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [active]);
  return posRef;
}

function Card({ task, project }) {
  const { drag, setDrag } = React.useContext(DragCtx);
  const fields = project.fields;
  const visibleChips = fields.filter((f) => task.values[f.id] !== undefined && task.values[f.id] !== '' && f.showOnCard !== false);
  const subDone = task.subtasks.filter((s) => s.done).length;
  const subTotal = task.subtasks.length;
  const depsIn = (task.depsIn || []).length;
  const depsOut = (task.depsOut || []).length;
  const blockingCount = (task.depsIn || []).filter((id) => { const d = getTask(id); return d && !d.done; }).length;
  const parentInfo = getParentInfo(task);
  const epicProgress = task.isEpic ? getEpicProgress(project, task.id) : null;
  const epicPct = epicProgress && epicProgress.total > 0 ? Math.round(epicProgress.done / epicProgress.total * 100) : 0;
  const storyProgress = task.isStory ? getStoryProgress(task.id) : null;
  const storyPct = storyProgress && storyProgress.total > 0 ? Math.round(storyProgress.done / storyProgress.total * 100) : 0;
  const childrenHidden = (task.isEpic && task.epicChildrenVisible === false) || (task.isStory && task.storyChildrenVisible === false);
  const cardStyle = task.isEpic
    ? { background: tint(task.epicColor || '#9a948a', 24), borderLeft: `8px solid ${task.epicColor || '#9a948a'}` }
    : task.isStory
      ? { background: tint(task.storyColor || '#7a9bc4', 30), borderLeft: `4px solid ${task.storyColor || '#7a9bc4'}` }
      : parentInfo
        ? { borderLeft: `3px solid ${parentInfo.color || '#9a948a'}` }
        : undefined;

  return (
    <div
      className={'card' + (drag?.taskId === task.id ? ' dragging' : '') + (task.isEpic ? ' is-epic' : '') + (task.isStory ? ' is-story' : '')}
      style={cardStyle}
      draggable
      onDragStart={(e) => {
        setDrag({ taskId: task.id });
        e.dataTransfer.setData('text/plain', task.id);
        e.dataTransfer.effectAllowed = 'move';
      }}
      onDragEnd={() => setDrag(null)}
      onClick={() => actions.setActiveTask(task.id)}>
      
      {(task.isEpic || task.isStory) && (
        <div className="card-top-row">
          <button className="epic-toggle-btn" onClick={(e) => {
            e.stopPropagation();
            if (task.isEpic) actions.setEpicChildrenVisible(task.id, task.epicChildrenVisible === false);
            else actions.setStoryChildrenVisible(task.id, task.storyChildrenVisible === false);
          }} title={childrenHidden ? 'Show linked cards on board' : 'Hide linked cards from board'}>
            {childrenHidden ? '👁' : '🙈'}
          </button>
        </div>
      )}
      {task.isEpic && <div className="epic-tag">◆ EPIC</div>}
      {task.isStory && <div className="epic-tag">◇ STORY</div>}
      {parentInfo && (
        <button className="parent-jump-btn" onClick={(e) => { e.stopPropagation(); actions.setActiveTask(parentInfo.id); }}
          title={`Jump to ${parentInfo.label}: ${parentInfo.title}`}>
          ↥ {parentInfo.title}
        </button>
      )}
      <div className="card-title-row">
        <label className="card-done-checkbox" onClick={(e) => e.stopPropagation()} title="Mark done">
          <input type="checkbox" checked={!!task.done} onChange={(e) => actions.setTaskDone(task.id, e.target.checked)} />
        </label>
        <div className={'card-title' + (task.done ? ' is-done' : '')}>{task.title}</div>
      </div>
      {visibleChips.length > 0 &&
      <div className="card-fields">
          {visibleChips.slice(0, 5).map((f) => <FieldChip key={f.id} field={f} value={task.values[f.id]} project={project} />)}
        </div>
      }
      {(subTotal > 0 || blockingCount > 0) &&
      <div className="card-meta">
          {subTotal > 0 && <span>☑ {subDone}/{subTotal}</span>}
          {blockingCount > 0 && <span title={`${blockingCount} in-dependency not done`} className="meta-warn">⚠ {blockingCount}</span>}
        </div>
      }
      {task.isEpic && epicProgress && epicProgress.total > 0 &&
      <div className="epic-progress-bar mini">
          <div className="epic-progress-fill" style={{ width: epicPct + '%', background: task.epicColor || '#9a948a' }} />
        </div>
      }
      {task.isStory && storyProgress && storyProgress.total > 0 &&
      <div className="epic-progress-bar mini">
          <div className="epic-progress-fill" style={{ width: storyPct + '%', background: task.storyColor || '#7a9bc4' }} />
        </div>
      }
    </div>);

}

function Column({ project, stage, tasks, laneKey, isFirstLane }) {
  const { drag, setDrag } = React.useContext(DragCtx);
  const [over, setOver] = React.useState(false);
  const [colDragOver, setColDragOver] = React.useState(false);
  const [adding, setAdding] = React.useState(false);
  const [newTitle, setNewTitle] = React.useState('');
  const [renaming, setRenaming] = React.useState(false);
  const [draftName, setDraftName] = React.useState(stage.name);
  const [menuOpen, setMenuOpen] = React.useState(false);
  const menuRef = React.useRef(null);

  // Collapsed state: only explicit user flag — no auto-collapse on empty.
  const isCollapsed = stage.collapsed === true;

  React.useEffect(() => {setDraftName(stage.name);}, [stage.name]);
  React.useEffect(() => {
    if (!menuOpen) return;
    function onDoc(e) {if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);}
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [menuOpen]);

  const sorted = [...tasks].sort((a, b) => a.order - b.order);
  const [dropIndex, setDropIndex] = React.useState(null);

  // Single source of truth for insertion index: scan actual card rects on every
  // dragover so gaps between cards resolve to the nearest boundary instead of
  // racing with a separate per-card handler (which caused jitter).
  function handleCardsDragOver(e) {
    if (drag?.kind === 'column') return;
    e.preventDefault();
    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
    setOver(true);
    const cardEls = [...e.currentTarget.querySelectorAll(':scope > .card-slot > .card')];
    let idx = cardEls.length;
    for (let i = 0; i < cardEls.length; i++) {
      const rect = cardEls[i].getBoundingClientRect();
      if (e.clientY < rect.top + rect.height / 2) { idx = i; break; }
    }
    setDropIndex(idx);
  }

  function onDrop(e) {
    e.preventDefault();e.stopPropagation();
    setOver(false);setColDragOver(false);
    if (drag?.kind === 'column') {setDrag(null);setDropIndex(null);return;}
    const id = e.dataTransfer.getData('text/plain') || drag?.taskId;
    if (!id) { setDropIndex(null); return; }
    // dropIndex was scanned against the VISIBLE (filtered/collapsed) card
    // list rendered in this column, which can be a strict subset of the
    // stage's actual full task list (epics/stories collapse children,
    // search/hide-done filters hide others, etc). A raw numeric index from
    // that visible subset does NOT correspond to the same position in the
    // full list moveTask() reorders — so instead of a number, resolve a
    // neighbor task id from the visible list and let moveTask() find that
    // neighbor's real position in the full per-stage list, leaving any
    // hidden tasks' relative order untouched.
    const others = sorted.filter(t => t.id !== id);
    let idx = dropIndex !== null ? dropIndex : sorted.length;
    const originalIdx = sorted.findIndex(t => t.id === id);
    if (originalIdx !== -1 && idx > originalIdx) idx -= 1;
    idx = Math.max(0, Math.min(idx, others.length));
    const beforeId = others[idx]?.id ?? null;
    const afterId = idx > 0 ? others[idx - 1]?.id : null;
    actions.moveTask(id, stage.id, { beforeId, afterId });
    if (laneKey && laneKey !== '__empty__') {
      if (project.groupBy === '__epic__') {
        actions.setTaskEpic(id, laneKey);
      } else if (project.groupBy === '__story__') {
        actions.setTaskStory(id, laneKey);
      } else {
        const groupField = project.fields.find((f) => f.id === project.groupBy);
        if (project.groupBy !== 'none' && groupField?.type === 'select') {
          actions.setTaskValue(id, project.groupBy, laneKey);
        }
      }
    }
    setDropIndex(null);
    setDrag(null);
  }

  function commitNew(continueAfter) {
    const v = newTitle.trim();
    if (v) {
      const id = actions.addTask(project.id, stage.id, v);
      if (laneKey && laneKey !== '__empty__') {
        if (project.groupBy === '__epic__') {
          actions.setTaskEpic(id, laneKey);
        } else if (project.groupBy === '__story__') {
          actions.setTaskStory(id, laneKey);
        } else {
          const groupField = project.fields.find((f) => f.id === project.groupBy);
          if (project.groupBy !== 'none' && groupField?.type === 'select') {
            actions.setTaskValue(id, project.groupBy, laneKey);
          }
        }
      }
    }
    setNewTitle('');
    if (!continueAfter || !v) setAdding(false);
  }
  function commitRename() {
    const v = draftName.trim();
    if (v && v !== stage.name) actions.updateStage(project.id, stage.id, { name: v });else
    setDraftName(stage.name);
    setRenaming(false);
  }

  const colDraggable = isFirstLane !== false;
  function onColDragStart(e) {
    if (!colDraggable) return;
    setDrag({ kind: 'column', stageId: stage.id });
    e.dataTransfer.setData('text/plain', '__col__:' + stage.id);
    e.dataTransfer.effectAllowed = 'move';
    e.stopPropagation();
  }
  function onColDragOver(e) {
    if (drag?.kind === 'column' && drag.stageId !== stage.id) {
      e.preventDefault();e.stopPropagation();
      if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
      setColDragOver(true);
    }
  }
  function onColDrop(e) {
    if (drag?.kind === 'column' && drag.stageId !== stage.id) {
      e.preventDefault();e.stopPropagation();
      const fromIdx = project.stages.findIndex((s) => s.id === drag.stageId);
      const toIdx = project.stages.findIndex((s) => s.id === stage.id);
      if (fromIdx !== -1 && toIdx !== -1) actions.reorderStages(project.id, fromIdx, toIdx);
      setColDragOver(false);setDrag(null);
    }
  }

  if (isCollapsed) {
    return (
      <div
        className={'column collapsed' + (over ? ' drop-target' : '')}
        onDragOver={(e) => {
          if (drag?.kind === 'column') return;
          e.preventDefault();
          if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
          setOver(true);
        }}
        onDragLeave={(e) => { if (e.currentTarget.contains(e.relatedTarget)) return; setOver(false); }}
        onDrop={onDrop}
        title={stage.name + ' — click to expand'}>
        
        <button className="col-expand-btn" onClick={() => actions.setStageCollapsed(project.id, stage.id, false)}>»</button>
        <div className="col-collapsed-name">{stage.name}</div>
        <span className="count col-collapsed-count">{sorted.length}</span>
      </div>);

  }

  return (
    <div
      className={'column' + (over ? ' drop-target' : '') + (colDragOver ? ' col-drop-target' : '')}
      onDragOver={(e) => {
        if (drag?.kind === 'column') return;
        e.preventDefault();
        if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
        setOver(true);
      }}
      onDragLeave={(e) => { if (e.currentTarget.contains(e.relatedTarget)) return; setOver(false); setDropIndex(null); }}
      onDrop={onDrop}>
      
      <div
        className={'col-head' + (colDraggable ? ' col-head-draggable' : '')}
        draggable={colDraggable && !renaming}
        onDragStart={onColDragStart}
        onDragOver={onColDragOver}
        onDragLeave={(e) => { if (e.currentTarget.contains(e.relatedTarget)) return; setColDragOver(false); }}
        onDrop={onColDrop}
        onDragEnd={() => {setDrag(null);setColDragOver(false);}}>
        
        {renaming ?
        <input autoFocus className="col-rename" value={draftName}
        onChange={(e) => setDraftName(e.target.value)} onBlur={commitRename}
        onKeyDown={(e) => {
          if (e.key === 'Enter') commitRename();
          if (e.key === 'Escape') {setDraftName(stage.name);setRenaming(false);}
        }} /> :

        <span className="col-name" onClick={() => {if (colDraggable) setRenaming(true);}}
        title={colDraggable ? 'Click to rename, drag to reorder' : ''}>{stage.name}</span>
        }
        <span className="col-head-right">
          <span className="count">{sorted.length}</span>
          <button className="col-collapse-btn" title="Collapse column"
          onClick={() => actions.setStageCollapsed(project.id, stage.id, true)}>«</button>
          {colDraggable &&
          <span className="col-menu-wrap" ref={menuRef}>
              <button className="col-menu-btn" aria-label="Column menu"
            onClick={(e) => {e.stopPropagation();setMenuOpen((o) => !o);}}>⋯</button>
              {menuOpen &&
            <div className="col-menu" onClick={(e) => e.stopPropagation()}>
                  <button onClick={() => {setMenuOpen(false);setRenaming(true);}}>Rename</button>
                  <button onClick={() => {setMenuOpen(false);setAdding(true);}}>Add task</button>
                  <button onClick={() => {setMenuOpen(false);actions.setStageCollapsed(project.id, stage.id, true);}}>Collapse</button>
                  <button
                onClick={() => {
                  setMenuOpen(false);
                  if (project.stages.length <= 1) {alert('A board needs at least one column.');return;}
                  const taskCount = sorted.length;
                  const msg = taskCount > 0 ?
                  `Delete "${stage.name}"? ${taskCount} task${taskCount === 1 ? '' : 's'} will move to "${project.stages.find((s) => s.id !== stage.id)?.name}".` :
                  `Delete "${stage.name}"?`;
                  if (confirm(msg)) actions.deleteStage(project.id, stage.id);
                }}
                className="danger">Delete column</button>
                </div>
            }
            </span>
          }
        </span>
      </div>
      <div className="col-cards" onDragOver={handleCardsDragOver}>
        {sorted.map((t, i) => (
          <div key={t.id} className="card-slot">
            {drag && drag.kind !== 'column' && dropIndex === i && <div className="drop-indicator" />}
            <Card task={t} project={project} />
          </div>
        ))}
        <div className="card-slot card-slot-end">
          {drag && drag.kind !== 'column' && dropIndex === sorted.length && <div className="drop-indicator" />}
        </div>
        {adding ?
        <div className="card">
            <input autoFocus
          style={{ width: '100%', border: 'none', background: 'transparent', font: 'inherit' }}
          placeholder="Task title…" value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)} onBlur={() => commitNew(false)}
          onKeyDown={(e) => {if (e.key === 'Enter') {e.preventDefault();commitNew(true);}if (e.key === 'Escape') {setAdding(false);setNewTitle('');}}} />
          </div> :

        <div className="add-card" onClick={() => setAdding(true)}>+ Add task</div>
        }
      </div>
    </div>);

}

function AddColumn({ project }) {
  const [adding, setAdding] = React.useState(false);
  const [name, setName] = React.useState('');
  function commit() {
    const v = name.trim();if (v) actions.addStage(project.id, v);
    setName('');setAdding(false);
  }
  if (!adding) {
    return (
      <button className="add-column" onClick={() => setAdding(true)}>
        <span className="add-column-plus">+</span><span>Add column</span>
      </button>);

  }
  return (
    <div className="add-column adding">
      <input autoFocus placeholder="Column name…" value={name}
      onChange={(e) => setName(e.target.value)} onBlur={commit}
      onKeyDown={(e) => {if (e.key === 'Enter') commit();if (e.key === 'Escape') {setName('');setAdding(false);}}} />
    </div>);

}

function taskInLane(task, project, fieldId, laneKey) {
  if (fieldId === '__epic__') {
    if (task.isEpic) return false;
    return laneKey === '__empty__' ? !task.epicId : task.epicId === laneKey;
  }
  if (fieldId === '__story__') {
    if (task.isEpic || task.isStory) return false;
    return laneKey === '__empty__' ? !task.storyId : task.storyId === laneKey;
  }
  const field = project.fields.find((f) => f.id === fieldId);
  if (!field) return false;
  const v = task.values[fieldId];
  if (field.type === 'multi_select') {
    const arr = Array.isArray(v) ? v : (v ? [v] : []);
    return laneKey === '__empty__' ? arr.length === 0 : arr.includes(laneKey);
  }
  if (field.type === 'checkbox') {
    return laneKey === '__true__' ? !!v : !v;
  }
  return (v || '__empty__') === laneKey;
}

function laneValuesForField(project, fieldId, tasks) {
  if (fieldId === '__epic__') {
    const epics = getEpics(project.id);
    const lanes = epics.map(e => ({
      key: e.id, label: e.title,
      count: tasks.filter(t => t.epicId === e.id).length,
    }));
    const unsetCount = tasks.filter(t => !t.epicId && !t.isEpic).length;
    if (unsetCount > 0) lanes.push({ key: '__empty__', label: 'No epic', count: unsetCount });
    return lanes;
  }
  if (fieldId === '__story__') {
    const stories = getStories(project.id);
    const lanes = stories.map(st => ({
      key: st.id, label: st.title,
      count: tasks.filter(t => t.storyId === st.id).length,
    }));
    const unsetCount = tasks.filter(t => !t.storyId && !t.isEpic && !t.isStory).length;
    if (unsetCount > 0) lanes.push({ key: '__empty__', label: 'No story', count: unsetCount });
    return lanes;
  }
  const field = project.fields.find((f) => f.id === fieldId);
  if (!field) return [];
  if (field.type === 'select' && field.options) {
    const lanes = field.options.map((o) => ({ key: o, label: o, count: tasks.filter((t) => t.values[fieldId] === o).length }));
    const unsetCount = tasks.filter((t) => !t.values[fieldId]).length;
    if (unsetCount > 0) lanes.push({ key: '__empty__', label: 'No ' + field.name, count: unsetCount });
    return lanes;
  }
  if (field.type === 'multi_select') {
    const optionOrder = field.options || [];
    const seen = new Set();
    const ordered = [];
    tasks.forEach((t) => {
      const arr = Array.isArray(t.values[fieldId]) ? t.values[fieldId] : (t.values[fieldId] ? [t.values[fieldId]] : []);
      arr.forEach((v) => { if (!seen.has(v)) { seen.add(v); ordered.push(v); } });
    });
    const values = [...optionOrder, ...ordered.filter((v) => !optionOrder.includes(v))];
    const lanes = values.map((v) => ({
      key: v, label: v,
      count: tasks.filter((t) => (Array.isArray(t.values[fieldId]) ? t.values[fieldId] : (t.values[fieldId] ? [t.values[fieldId]] : [])).includes(v)).length,
    })).filter((l) => l.count > 0 || optionOrder.includes(l.key));
    const unsetCount = tasks.filter((t) => !(Array.isArray(t.values[fieldId]) ? t.values[fieldId] : []).length).length;
    if (unsetCount > 0) lanes.push({ key: '__empty__', label: 'No ' + field.name, count: unsetCount });
    return lanes;
  }
  if (field.type === 'checkbox') {
    const yes = tasks.filter((t) => !!t.values[fieldId]).length;
    const no = tasks.filter((t) => !t.values[fieldId]).length;
    const lanes = [];
    if (yes > 0) lanes.push({ key: '__true__', label: field.name, count: yes });
    if (no > 0) lanes.push({ key: '__empty__', label: 'No ' + field.name, count: no });
    return lanes;
  }
  const distinct = [...new Set(tasks.map((t) => t.values[fieldId] || '__empty__'))];
  return distinct.map((v) => ({ key: v, label: v === '__empty__' ? 'No ' + field.name : v,
    count: tasks.filter((t) => (t.values[fieldId] || '__empty__') === v).length }));
}

function BoardWrap({ groupBy, children }) {
  const { drag } = React.useContext(DragCtx);
  const wrapRef = React.useRef(null);
  const posRef = useEdgeAutoscroll(wrapRef, !!drag);
  return (
    <div className="board-wrap" ref={wrapRef} style={groupBy !== 'none' ? {paddingLeft: 0} : undefined}
      onDragOver={(e) => {
        e.preventDefault();
        if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
        posRef.current = { x: e.clientX, y: e.clientY };
      }}>
      {children}
    </div>
  );
}

function BoardScreen() {
  const s = useStore();
  const project = s.projects.find((p) => p.id === s.activeProjectId);
  if (!project) return null;
  const allTasks = s.tasks.filter((t) => t.projectId === project.id);
  const groupBy = project.groupBy || 'none';
  const tasks = applyFilters(project, allTasks, { ignoreCollapse: groupBy === '__epic__' || groupBy === '__story__' });
  const groupField = groupBy === 'none' ? null : project.fields.find((f) => f.id === groupBy);

  return (
    <DragProvider>
      <BoardWrap groupBy={groupBy}>
        {groupBy === 'none' ?
        <div className="board">
            <div className="board-columns">
              {project.stages.map((stage) =>
            <Column key={stage.id} project={project} stage={stage}
            tasks={tasks.filter((t) => t.stageId === stage.id)} laneKey={null} />
            )}
            </div>
            <AddColumn project={project} />
          </div> :

        <div className="board has-lanes">
            <div className="lane-stack">
              {laneValuesForField(project, groupBy, tasks).map((lane, laneIdx) => {
              const laneTasks = tasks.filter((t) => taskInLane(t, project, groupBy, lane.key));
              return (
                <div className="lane-row" key={lane.key}>
                    <div className="lane-label">
                      <span>{lane.label}</span>
                      <span className="lane-meta">{lane.count} {lane.count === 1 ? 'task' : 'tasks'}</span>
                    </div>
                    {project.stages.map((stage) =>
                  <Column key={stage.id} project={project} stage={stage}
                  tasks={laneTasks.filter((t) => t.stageId === stage.id)}
                  laneKey={lane.key} isFirstLane={laneIdx === 0} />
                  )}
                  </div>);

            })}
            </div>
            <AddColumn project={project} />
          </div>
        }
      </BoardWrap>
    </DragProvider>);

}

window.BoardScreen = BoardScreen;
window.laneValuesForField = laneValuesForField;