// Tasks module — Boards / Sprints / User stories / Tasks & bugs (Azure DevOps style).
// State lives in the screen; everything is persisted via window.api.{boards,sprints,stories,workItems}.
const { useState: useStateT, useEffect: useEffectT, useMemo: useMemoT, useCallback: useCallbackT, useRef: useRefT } = React;

// ────────────────────────────────────────────────────────────────────────────────
// Drag & drop payload (HTML5 dataTransfer is string-only).
// ────────────────────────────────────────────────────────────────────────────────
const DND_MIME = "application/x-vildez-workitem+json";

// ────────────────────────────────────────────────────────────────────────────────
// WorkItemType: backend serializes enums as int (System.Text.Json default).
// We use the string form ("Task" / "Bug") in the React tree for readability and
// translate at the API boundary, mirroring how invoices.jsx handles InvoiceStatus.
// ────────────────────────────────────────────────────────────────────────────────
const WORK_ITEM_TYPE_BY_INT = { 0: "Task", 1: "Bug" };
const WORK_ITEM_TYPE_BY_NAME = { Task: 0, Bug: 1 };
const normalizeItem = (it) => ({
  ...it,
  type: typeof it.type === "number" ? (WORK_ITEM_TYPE_BY_INT[it.type] ?? "Task") : it.type,
});
const normalizeView = (v) => v && {
  ...v,
  stories: (v.stories || []).map(s => ({ ...s, items: (s.items || []).map(normalizeItem) })),
};

// ────────────────────────────────────────────────────────────────────────────────
// Modals
// ────────────────────────────────────────────────────────────────────────────────

function BoardModal({ open, onClose, onSubmit, t, initial }) {
  const [name, setName] = useStateT("");
  const [description, setDescription] = useStateT("");
  const [busy, setBusy] = useStateT(false);
  useEffectT(() => {
    if (open) {
      setName(initial?.name || "");
      setDescription(initial?.description || "");
      setBusy(false);
    }
  }, [open, initial]);

  const submit = async () => {
    if (!name.trim() || busy) return;
    setBusy(true);
    try {
      await onSubmit({ name: name.trim(), description: description.trim() || null });
      onClose();
    } finally {
      setBusy(false);
    }
  };

  return (
    <window.Modal
      open={open}
      onClose={onClose}
      title={initial ? t("edit_board") : t("new_board")}
      width={460}
      footer={
        <>
          <button className="btn ghost" onClick={onClose} disabled={busy}>{t("cancel")}</button>
          <button className="btn brand" onClick={submit} disabled={busy || !name.trim()}>
            {initial ? t("save") : t("create")}
          </button>
        </>
      }
    >
      <div className="form-row">
        <label className="form-label">{t("name")}</label>
        <input className="input" value={name} onChange={e => setName(e.target.value)} maxLength={80} autoFocus />
      </div>
      <div className="form-row">
        <label className="form-label">{t("description")}</label>
        <textarea className="input" value={description} onChange={e => setDescription(e.target.value)} maxLength={500} rows={3} />
      </div>
    </window.Modal>
  );
}

function SprintModal({ open, onClose, onSubmit, t, initial }) {
  const [name, setName] = useStateT("");
  const [goal, setGoal] = useStateT("");
  const [startDate, setStartDate] = useStateT("");
  const [endDate, setEndDate] = useStateT("");
  const [busy, setBusy] = useStateT(false);
  useEffectT(() => {
    if (open) {
      setName(initial?.name || "");
      setGoal(initial?.goal || "");
      setStartDate(initial?.startDate || "");
      setEndDate(initial?.endDate || "");
      setBusy(false);
    }
  }, [open, initial]);

  const submit = async () => {
    if (!name.trim() || busy) return;
    setBusy(true);
    try {
      await onSubmit({
        name: name.trim(),
        goal: goal.trim() || null,
        startDate: startDate || null,
        endDate: endDate || null,
      });
      onClose();
    } finally { setBusy(false); }
  };

  return (
    <window.Modal
      open={open}
      onClose={onClose}
      title={initial ? t("edit_sprint") : t("new_sprint")}
      width={500}
      footer={
        <>
          <button className="btn ghost" onClick={onClose} disabled={busy}>{t("cancel")}</button>
          <button className="btn brand" onClick={submit} disabled={busy || !name.trim()}>
            {initial ? t("save") : t("create")}
          </button>
        </>
      }
    >
      <div className="form-row">
        <label className="form-label">{t("name")}</label>
        <input className="input" value={name} onChange={e => setName(e.target.value)} maxLength={80} autoFocus />
      </div>
      <div className="form-row">
        <label className="form-label">{t("goal")}</label>
        <input className="input" value={goal} onChange={e => setGoal(e.target.value)} maxLength={280} />
      </div>
      <div className="form-row" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
        <div>
          <label className="form-label">{t("start_date")}</label>
          <window.DatePicker value={startDate} onChange={setStartDate} />
        </div>
        <div>
          <label className="form-label">{t("end_date")}</label>
          <window.DatePicker value={endDate} onChange={setEndDate} />
        </div>
      </div>
    </window.Modal>
  );
}

function StoryModal({ open, onClose, onSubmit, t, initial, members, lang }) {
  const [title, setTitle] = useStateT("");
  const [description, setDescription] = useStateT("");
  const [assigneeId, setAssigneeId] = useStateT("");
  const [busy, setBusy] = useStateT(false);
  useEffectT(() => {
    if (open) {
      setTitle(initial?.title || "");
      setDescription(initial?.description || "");
      setAssigneeId(initial?.assigneeTeamMemberId || "");
      setBusy(false);
    }
  }, [open, initial]);

  const submit = async () => {
    if (!title.trim() || busy) return;
    setBusy(true);
    try {
      await onSubmit({
        title: title.trim(),
        description: description.trim() || null,
        assigneeTeamMemberId: assigneeId || null,
      });
      onClose();
    } finally { setBusy(false); }
  };

  const memberOptions = [
    { value: "", label: t("unassigned") },
    ...(members || []).map(m => ({ value: m.id, label: m.name })),
  ];

  return (
    <window.Modal
      open={open}
      onClose={onClose}
      title={initial ? t("edit_story") : t("new_story")}
      width={520}
      footer={
        <>
          <button className="btn ghost" onClick={onClose} disabled={busy}>{t("cancel")}</button>
          <button className="btn brand" onClick={submit} disabled={busy || !title.trim()}>
            {initial ? t("save") : t("create")}
          </button>
        </>
      }
    >
      <div className="form-row">
        <label className="form-label">{t("title")}</label>
        <input className="input" value={title} onChange={e => setTitle(e.target.value)} maxLength={200} autoFocus />
      </div>
      <div className="form-row">
        <label className="form-label">{t("description")}</label>
        <textarea className="input" value={description} onChange={e => setDescription(e.target.value)} maxLength={4000} rows={4} />
      </div>
      <div className="form-row">
        <label className="form-label">{t("assignee")}</label>
        <window.Select value={assigneeId} onChange={setAssigneeId} options={memberOptions} />
      </div>
    </window.Modal>
  );
}

function WorkItemModal({ open, onClose, onSubmit, t, initial, defaultType, members }) {
  const [type, setType] = useStateT("Task");
  const [title, setTitle] = useStateT("");
  const [description, setDescription] = useStateT("");
  const [assigneeId, setAssigneeId] = useStateT("");
  const [busy, setBusy] = useStateT(false);
  useEffectT(() => {
    if (open) {
      setType(initial?.type || defaultType || "Task");
      setTitle(initial?.title || "");
      setDescription(initial?.description || "");
      setAssigneeId(initial?.assigneeTeamMemberId || "");
      setBusy(false);
    }
  }, [open, initial, defaultType]);

  const submit = async () => {
    if (!title.trim() || busy) return;
    setBusy(true);
    try {
      // The backend always seats new work items in the board's initial state
      // (the spec says items are created in the backlog only), so we don't send a stateId.
      const payload = {
        title: title.trim(),
        description: description.trim() || null,
        assigneeTeamMemberId: assigneeId || null,
      };
      if (!initial) payload.type = type;
      await onSubmit(payload);
      onClose();
    } finally { setBusy(false); }
  };

  const memberOptions = [
    { value: "", label: t("unassigned") },
    ...(members || []).map(m => ({ value: m.id, label: m.name })),
  ];
  const typeOptions = [
    { value: "Task", label: t("task") },
    { value: "Bug",  label: t("bug") },
  ];

  return (
    <window.Modal
      open={open}
      onClose={onClose}
      title={initial ? t("edit_item") : (defaultType === "Bug" ? t("new_bug") : t("new_task"))}
      width={520}
      footer={
        <>
          <button className="btn ghost" onClick={onClose} disabled={busy}>{t("cancel")}</button>
          <button className="btn brand" onClick={submit} disabled={busy || !title.trim()}>
            {initial ? t("save") : t("create")}
          </button>
        </>
      }
    >
      {!initial && (
        <div className="form-row">
          <label className="form-label">{t("type")}</label>
          <window.Select value={type} onChange={setType} options={typeOptions} />
        </div>
      )}
      <div className="form-row">
        <label className="form-label">{t("title")}</label>
        <input className="input" value={title} onChange={e => setTitle(e.target.value)} maxLength={200} autoFocus />
      </div>
      <div className="form-row">
        <label className="form-label">{t("description")}</label>
        <textarea className="input" value={description} onChange={e => setDescription(e.target.value)} maxLength={4000} rows={4} />
      </div>
      <div className="form-row">
        <label className="form-label">{t("assignee")}</label>
        <window.Select value={assigneeId} onChange={setAssigneeId} options={memberOptions} />
      </div>
    </window.Modal>
  );
}

function BoardSettingsModal({ open, onClose, board, onChanged, t, lang }) {
  // Local snapshot so we can apply edits incrementally and refresh the board afterwards.
  const [states, setStates] = useStateT([]);
  const [busy, setBusy] = useStateT(false);
  const [pendingDelete, setPendingDelete] = useStateT(null); // { stateId, count, reassignTo }
  const [error, setError] = useStateT(null);
  const toast = window.useToast();

  useEffectT(() => {
    if (open && board) {
      setStates([...board.states].sort((a, b) => a.order - b.order));
      setPendingDelete(null);
      setError(null);
    }
  }, [open, board]);

  const updateLocal = (id, patch) =>
    setStates(s => s.map(x => x.id === id ? { ...x, ...patch } : x));

  const saveState = async (st) => {
    setBusy(true); setError(null);
    try {
      await window.api.boardStates.update(board.id, st.id, {
        name: st.name, isInitial: st.isInitial, isTerminal: st.isTerminal,
      });
      toast(t("save"), "success");
      await onChanged?.();
    } catch (e) { setError(e.message); }
    finally { setBusy(false); }
  };

  const addState = async () => {
    setBusy(true); setError(null);
    try {
      const created = await window.api.boardStates.add(board.id, {
        name: lang === "es" ? "Nueva columna" : "New column",
        isInitial: false,
        isTerminal: false,
      });
      setStates(s => [...s, created]);
      await onChanged?.();
    } catch (e) { setError(e.message); }
    finally { setBusy(false); }
  };

  const requestDelete = (st) => {
    setPendingDelete({ stateId: st.id, name: st.name, reassignTo: "" });
  };

  const confirmDelete = async () => {
    if (!pendingDelete) return;
    setBusy(true); setError(null);
    try {
      await window.api.boardStates.delete(board.id, pendingDelete.stateId, pendingDelete.reassignTo || null);
      setStates(s => s.filter(x => x.id !== pendingDelete.stateId));
      setPendingDelete(null);
      await onChanged?.();
    } catch (e) {
      // Backend asks for reassignTo when items exist. Make the dropdown required and surface the message.
      setError(e.message);
    }
    finally { setBusy(false); }
  };

  const reorder = async (fromIdx, toIdx) => {
    if (fromIdx === toIdx) return;
    const next = [...states];
    const [moved] = next.splice(fromIdx, 1);
    next.splice(toIdx, 0, moved);
    setStates(next);
    setBusy(true); setError(null);
    try {
      await window.api.boardStates.reorder(board.id, next.map(s => s.id));
      await onChanged?.();
    } catch (e) { setError(e.message); }
    finally { setBusy(false); }
  };

  if (!open || !board) return null;

  const otherStateOptions = pendingDelete
    ? states.filter(s => s.id !== pendingDelete.stateId).map(s => ({ value: s.id, label: s.name }))
    : [];

  return (
    <window.Modal
      open={open}
      onClose={onClose}
      title={t("board_settings")}
      subtitle={board.name}
      width={580}
      footer={<button className="btn ghost" onClick={onClose}>{t("cancel")}</button>}
    >
      <h4 style={{ margin: "0 0 8px", fontSize: 13, fontWeight: 600 }}>{t("manage_states")}</h4>
      <div className="kanban-states-list">
        {states.map((st, idx) => (
          <div key={st.id} className="kanban-state-row">
            <span className="kanban-state-grip" title="Reorder">⠿</span>
            <input
              className="input"
              value={st.name}
              onChange={e => updateLocal(st.id, { name: e.target.value })}
              onBlur={() => saveState(st)}
              maxLength={40}
              style={{ flex: 1, minWidth: 0 }}
            />
            <label className="kanban-state-flag">
              <input type="checkbox" checked={st.isInitial} onChange={e => { updateLocal(st.id, { isInitial: e.target.checked }); saveState({ ...st, isInitial: e.target.checked }); }} />
              {t("is_initial")}
            </label>
            <label className="kanban-state-flag">
              <input type="checkbox" checked={st.isTerminal} onChange={e => { updateLocal(st.id, { isTerminal: e.target.checked }); saveState({ ...st, isTerminal: e.target.checked }); }} />
              {t("is_terminal")}
            </label>
            <button className="btn sm ghost" onClick={() => idx > 0 && reorder(idx, idx - 1)} disabled={idx === 0 || busy} title="Up">↑</button>
            <button className="btn sm ghost" onClick={() => idx < states.length - 1 && reorder(idx, idx + 1)} disabled={idx === states.length - 1 || busy} title="Down">↓</button>
            <button className="btn sm danger" onClick={() => requestDelete(st)} disabled={busy} title={t("delete_state")}>×</button>
          </div>
        ))}
      </div>
      <button className="btn" onClick={addState} disabled={busy} style={{ marginTop: 8 }}>{t("add_state")}</button>

      {pendingDelete && (
        <div className="kanban-confirm-strip">
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 600 }}>{t("delete_state")} "{pendingDelete.name}"?</div>
            <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 2 }}>
              {t("delete_state_has_items")}
            </div>
            <div style={{ marginTop: 6 }}>
              <label className="form-label">{t("reassign_items_to")}</label>
              <window.Select
                value={pendingDelete.reassignTo}
                onChange={(v) => setPendingDelete(p => ({ ...p, reassignTo: v }))}
                options={[{ value: "", label: "—" }, ...otherStateOptions]}
              />
            </div>
          </div>
          <div style={{ display: "flex", gap: 8, alignSelf: "flex-end" }}>
            <button className="btn ghost" onClick={() => setPendingDelete(null)} disabled={busy}>{t("cancel")}</button>
            <button className="btn danger" onClick={confirmDelete} disabled={busy}>{t("delete")}</button>
          </div>
        </div>
      )}

      {error && <div className="kanban-error">{error}</div>}
    </window.Modal>
  );
}

// ────────────────────────────────────────────────────────────────────────────────
// Kanban grid — rows = stories, cols = states. Tasks/bugs are draggable cards.
// ────────────────────────────────────────────────────────────────────────────────

function WorkItemCard({ item, members, t, onEdit, onDelete, onDragStart, onDragEnd }) {
  const member = members?.find(m => m.id === item.assigneeTeamMemberId);
  return (
    <div
      className="kanban-card"
      data-type={item.type === "Bug" ? "bug" : "task"}
      draggable
      onDragStart={(e) => onDragStart(e, item)}
      onDragEnd={onDragEnd}
      onDoubleClick={onEdit}
    >
      <div className="kanban-card-head">
        <span className="kanban-card-type">{item.type === "Bug" ? t("bug") : t("task")}</span>
        <button className="kanban-card-menu" onClick={(e) => { e.stopPropagation(); onDelete(); }} title={t("delete")} aria-label={t("delete")}>×</button>
      </div>
      <div className="kanban-card-title">{item.title}</div>
      {member && (
        <div className="kanban-card-foot">
          <window.Avatar size="sm" initials={member.initials || member.name?.slice(0, 2).toUpperCase()} />
          <span style={{ fontSize: 11, color: "var(--text-muted)" }}>{member.name}</span>
        </div>
      )}
    </div>
  );
}

function StoryRow({ story, items, states, members, t, onEditStory, onDeleteStory, onAddItem, onEditItem, onDeleteItem, onMove, dragState, setDragState }) {
  const assignee = members?.find(m => m.id === story.assigneeTeamMemberId);

  const onDragStart = (e, item) => {
    const payload = { itemId: item.id, storyId: story.id, fromStateId: item.stateId };
    e.dataTransfer.setData(DND_MIME, JSON.stringify(payload));
    e.dataTransfer.effectAllowed = "move";
    window.startDrag?.(e, e.currentTarget);
    setDragState(payload);
  };
  const onDragEnd = () => setDragState(null);

  const onDragOverCell = (e, stateId) => {
    if (!dragState) return;
    // Always preventDefault and report "move" so the browser keeps the grabbing
    // cursor everywhere. Invalid drops (cross-story) are filtered in onDrop —
    // the drop-invalid CSS class still gives the user the visual cue.
    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
  };

  const onDropCell = (e, stateId) => {
    const raw = e.dataTransfer.getData(DND_MIME);
    if (!raw) return;
    e.preventDefault();
    // Any drop ends the drag UX-wise — clear immediately so the cell pulses
    // don't linger when the optimistic re-render unmounts the source card and
    // its onDragEnd never fires.
    setDragState(null);
    let payload; try { payload = JSON.parse(raw); } catch { return; }
    // Cross-story drop attempts are silently ignored — the visual hatching on
    // the cell already told the user it wouldn't take.
    if (payload.storyId !== story.id) return;
    if (payload.fromStateId === stateId) return; // no-op
    onMove(payload.itemId, stateId);
  };

  const itemsByState = useMemoT(() => {
    const map = {};
    (states || []).forEach(s => { map[s.id] = []; });
    (items || []).forEach(it => {
      (map[it.stateId] || (map[it.stateId] = [])).push(it);
    });
    Object.values(map).forEach(list => list.sort((a, b) => a.order - b.order));
    return map;
  }, [items, states]);

  return (
    <>
      <div className="kanban-row-header">
        <div className="kanban-row-top">
          <div className="kanban-story-meta">
            <div className="kanban-story-title" onDoubleClick={onEditStory}>{story.title}</div>
            {assignee && (
              <div className="kanban-story-assignee" title={assignee.name}>
                <window.Avatar size="sm" initials={assignee.initials || assignee.name?.slice(0, 2).toUpperCase()} />
              </div>
            )}
          </div>
          <div className="kanban-story-actions">
            <button className="btn sm" onClick={onEditStory} title={t("edit_story")}><window.Icons.Edit /></button>
            <button className="btn sm danger" onClick={onDeleteStory} title={t("delete")}>×</button>
          </div>
        </div>
        {/* New work items always land in the initial column (backlog) — match
            Azure DevOps default and what the service enforces. The CTAs live on
            the story header so they're discoverable regardless of which column
            you're looking at. */}
        <div className="kanban-row-cta">
          <button className="kanban-add-btn" onClick={() => onAddItem(story, "Task")}>+ {t("task")}</button>
          <button className="kanban-add-btn alt" onClick={() => onAddItem(story, "Bug")}>+ {t("bug")}</button>
        </div>
      </div>
      {(states || []).map(st => {
        const dropInvalid = dragState && dragState.storyId !== story.id;
        const dropTarget = dragState && dragState.storyId === story.id;
        return (
          <div
            key={st.id}
            className={`kanban-cell ${dropInvalid ? "drop-invalid" : ""} ${dropTarget ? "drop-target" : ""}`}
            data-story-id={story.id}
            data-state-id={st.id}
            data-invalid-msg={dropInvalid ? t("drop_invalid_story") : undefined}
            onDragOver={(e) => onDragOverCell(e, st.id)}
            onDrop={(e) => onDropCell(e, st.id)}
          >
            {(itemsByState[st.id] || []).map(item => (
              <WorkItemCard
                key={item.id}
                item={item}
                members={members}
                t={t}
                onEdit={() => onEditItem(item)}
                onDelete={() => onDeleteItem(item)}
                onDragStart={onDragStart}
                onDragEnd={onDragEnd}
              />
            ))}
          </div>
        );
      })}
    </>
  );
}

// ────────────────────────────────────────────────────────────────────────────────
// Sprints list — the body the "sprints" subnav shows instead of the kanban.
// One row per sprint, with name, goal, date range, an active-now badge if it
// covers today, plus open / edit / delete actions.
// ────────────────────────────────────────────────────────────────────────────────

function SprintsListView({ sprints, t, lang, onEdit, onDelete, onOpen }) {
  if (!sprints || sprints.length === 0) {
    return (
      <div className="empty" style={{ marginTop: 12, padding: "40px 24px" }}>
        <div className="empty-icon"><window.Icons.Calendar /></div>
        <h3>{t("no_sprints")}</h3>
      </div>
    );
  }
  const fmt = (d) => d ? d : "—";
  return (
    <div className="card" style={{ overflow: "hidden", marginTop: 12 }}>
      <table className="tbl">
        <thead>
          <tr>
            <th>{t("name")}</th>
            <th>{t("goal")}</th>
            <th>{t("start_date")}</th>
            <th>{t("end_date")}</th>
            <th></th>
            <th style={{ textAlign: "right" }}></th>
          </tr>
        </thead>
        <tbody>
          {sprints.map(s => (
            <tr key={s.id} onDoubleClick={() => onOpen(s)} style={{ cursor: "pointer" }}>
              <td style={{ fontWeight: 600 }} onClick={() => onOpen(s)}>{s.name}</td>
              <td style={{ color: "var(--text-muted)" }}>{fmt(s.goal)}</td>
              <td style={{ color: "var(--text-muted)" }}>{fmt(s.startDate)}</td>
              <td style={{ color: "var(--text-muted)" }}>{fmt(s.endDate)}</td>
              <td>{s.isActive && <span className="badge success"><span className="dot" />{t("active_now")}</span>}</td>
              <td style={{ textAlign: "right" }}>
                <div style={{ display: "inline-flex", gap: 4 }} onClick={(e) => e.stopPropagation()}>
                  <button className="btn sm" onClick={() => onOpen(s)} title={t("boards")}><window.Icons.ArrowRight /></button>
                  <button className="btn sm" onClick={() => onEdit(s)} title={t("edit_sprint")}><window.Icons.Edit /></button>
                  <button className="btn sm danger" onClick={() => onDelete(s)} title={t("delete")}><window.Icons.Trash /></button>
                </div>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────────────
// In-app confirmation dialog. Replaces window.confirm() so the prompt looks like
// the rest of the app (and isn't gated by Chrome's "this site keeps asking…").
// ────────────────────────────────────────────────────────────────────────────────

function ConfirmDialog({ state, onClose, t }) {
  const [busy, setBusy] = useStateT(false);
  const onConfirm = async () => {
    if (busy || !state.onConfirm) return;
    setBusy(true);
    try { await state.onConfirm(); onClose(); }
    finally { setBusy(false); }
  };
  // Ensure we reset the busy flag whenever the dialog reopens for a new action.
  useEffectT(() => { if (state.open) setBusy(false); }, [state.open, state.onConfirm]);

  return (
    <window.Modal
      open={state.open}
      onClose={busy ? () => {} : onClose}
      title={state.title}
      width={420}
      closeOnBackdrop={!busy}
      footer={
        <>
          <button className="btn ghost" onClick={onClose} disabled={busy}>{t("cancel")}</button>
          <button className="btn danger" onClick={onConfirm} disabled={busy} autoFocus>
            {state.confirmLabel || t("delete")}
          </button>
        </>
      }
    >
      {state.detail && <p style={{ margin: 0, fontSize: 13, color: "var(--text-muted)", lineHeight: 1.5 }}>{state.detail}</p>}
    </window.Modal>
  );
}

// ────────────────────────────────────────────────────────────────────────────────
// Main screen
// ────────────────────────────────────────────────────────────────────────────────

window.TasksScreen = function TasksScreen({ t, lang, subTab, setSubTab, pathParts, setPathParts }) {
  const toast = window.useToast();

  const [boards, setBoards] = useStateT([]);
  // The URL owns board + sprint selection so the browser back button restores them.
  // pathParts[0] = boardId, pathParts[1] = sprintId. localStorage is a fallback for
  // the first visit where the URL is bare /tasks.
  const urlBoardId = (pathParts && pathParts[0]) || "";
  const urlSprintId = (pathParts && pathParts[1]) || "";
  const [fallbackBoardId, setFallbackBoardId] = useStateT(() => {
    try { return localStorage.getItem("companyBrain.tasks.boardId") || ""; }
    catch { return ""; }
  });
  const currentBoardId = urlBoardId || fallbackBoardId;
  const currentSprintId = urlSprintId;
  const setCurrentBoardId = useCallbackT((id, opts) => {
    setFallbackBoardId(id || "");
    setPathParts(id ? [id] : [], opts);
  }, [setPathParts]);
  const setCurrentSprintId = useCallbackT((id) => {
    setPathParts(prev => {
      const board = (prev && prev[0]) || currentBoardId;
      if (!board) return [];
      return id ? [board, id] : [board];
    });
  }, [setPathParts, currentBoardId]);
  const [sprints, setSprints] = useStateT([]);
  const [view, setView] = useStateT(null); // hydrated kanban payload
  const [members, setMembers] = useStateT([]);
  const [loading, setLoading] = useStateT(true);
  const [error, setError] = useStateT(null);
  const [refreshKey, setRefreshKey] = useStateT(0);
  const refresh = useCallbackT(() => setRefreshKey(k => k + 1), []);

  const [dragState, setDragState] = useStateT(null);
  // Safety net: if the drag ends without onDropCell firing (drop outside any
  // cell, ESC cancel, drag tab swap), clear the highlight state so cells stop
  // pulsing. The card-level onDragEnd is unreliable because the optimistic
  // re-render unmounts the source between drop and dragend dispatch.
  useEffectT(() => {
    const onDragEndDoc = () => setDragState(null);
    document.addEventListener("dragend", onDragEndDoc);
    return () => document.removeEventListener("dragend", onDragEndDoc);
  }, []);

  // Current user — used by the "mine" subnav to filter the kanban to items
  // assigned to the active actor.
  const auth = window.getAuth ? window.getAuth() : null;
  const currentUserId = auth?.member?.id || null;

  // The "backlog" subnav forces the sprint selector to "" (backlog view).
  useEffectT(() => {
    if (subTab === "backlog" && currentSprintId !== "") setCurrentSprintId("");
  }, [subTab, currentSprintId]);

  // Modals
  const [boardModal, setBoardModal] = useStateT({ open: false, initial: null });
  const [sprintModal, setSprintModal] = useStateT({ open: false, initial: null });
  const [storyModal, setStoryModal] = useStateT({ open: false, initial: null });
  const [itemModal, setItemModal] = useStateT({ open: false, initial: null, story: null, defaultType: "Task" });
  const [boardSettingsOpen, setBoardSettingsOpen] = useStateT(false);
  const [confirmState, setConfirmState] = useStateT({ open: false, title: "", detail: "", onConfirm: null });
  const closeConfirm = useCallbackT(() => setConfirmState(c => ({ ...c, open: false })), []);
  const askConfirm = useCallbackT((opts) => setConfirmState({ open: true, ...opts }), []);

  // Persist last board so reopening the screen lands the user back where they were.
  useEffectT(() => {
    try {
      if (currentBoardId) localStorage.setItem("companyBrain.tasks.boardId", currentBoardId);
      else localStorage.removeItem("companyBrain.tasks.boardId");
    } catch { /* ignore */ }
  }, [currentBoardId]);

  // Load boards + members on mount.
  useEffectT(() => {
    let cancelled = false;
    setLoading(true); setError(null);
    Promise.all([window.api.boards.list(), window.api.teamMembers.list()])
      .then(([bs, ms]) => {
        if (cancelled) return;
        setBoards(bs);
        setMembers(ms);
        if (bs.length > 0 && !bs.some(b => b.id === currentBoardId)) {
          // Auto-pick first board when nothing valid is selected. Use replace so
          // the back button doesn't land on a "no board" URL the user never saw.
          setCurrentBoardId(bs[0].id, { replace: true });
        }
      })
      .catch(e => { if (!cancelled) setError(e.message); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [refreshKey]);

  // Load sprints whenever the active board changes.
  useEffectT(() => {
    if (!currentBoardId) { setSprints([]); return; }
    let cancelled = false;
    window.api.sprints.listByBoard(currentBoardId)
      .then(list => { if (!cancelled) setSprints(list); })
      .catch(e => { if (!cancelled) toast(e.message, "danger"); });
    return () => { cancelled = true; };
  }, [currentBoardId, refreshKey]);

  // The kanban view is keyed by board + sprint (or backlog).
  useEffectT(() => {
    if (!currentBoardId) { setView(null); return; }
    let cancelled = false;
    window.api.boards.view(currentBoardId, currentSprintId || null)
      .then(v => { if (!cancelled) setView(normalizeView(v)); })
      .catch(e => { if (!cancelled) toast(e.message, "danger"); });
    return () => { cancelled = true; };
  }, [currentBoardId, currentSprintId, refreshKey]);

  const currentBoard = view?.board || boards.find(b => b.id === currentBoardId) || null;
  const states = currentBoard?.states || [];

  // ── Optimistic move handler. Updates local view immediately; reverts on failure. ──
  const moveItem = useCallbackT(async (itemId, toStateId) => {
    if (!view) return;
    const before = view;
    const target = view.stories.find(s => s.items.some(i => i.id === itemId));
    if (!target) return;
    const item = target.items.find(i => i.id === itemId);
    if (!item || item.stateId === toStateId) return;
    const targetOrder = target.items.filter(i => i.stateId === toStateId).length;

    // Optimistic update
    setView(v => ({
      ...v,
      stories: v.stories.map(s => s.story.id === target.story.id ? {
        ...s,
        items: s.items.map(i => i.id === itemId ? { ...i, stateId: toStateId, order: targetOrder } : i),
      } : s),
    }));
    try {
      await window.api.workItems.move(itemId, { stateId: toStateId, order: targetOrder });
    } catch (e) {
      setView(before);
      toast(e.message, "danger");
    }
  }, [view, toast]);

  // ── Mutations ──
  const createBoard = async (payload) => {
    const created = await window.api.boards.create(payload);
    setCurrentBoardId(created.id);
    setCurrentSprintId("");
    refresh();
    toast(t("save"), "success");
  };
  const editBoard = async (payload) => {
    await window.api.boards.update(currentBoardId, payload);
    refresh();
    toast(t("save"), "success");
  };
  const deleteBoard = () => {
    if (!currentBoard) return;
    askConfirm({
      title: t("delete_board_confirm"),
      detail: currentBoard.name,
      onConfirm: async () => {
        await window.api.boards.delete(currentBoard.id);
        setCurrentBoardId("");
        setCurrentSprintId("");
        refresh();
      },
    });
  };

  const createSprint = async (payload) => {
    const created = await window.api.sprints.create(currentBoardId, payload);
    setCurrentSprintId(created.id);
    refresh();
    toast(t("save"), "success");
  };
  const editSprint = async (payload) => {
    await window.api.sprints.update(currentSprintId, payload);
    refresh();
    toast(t("save"), "success");
  };
  const deleteSprint = () => {
    if (!currentSprintId) return;
    const sprint = sprints.find(s => s.id === currentSprintId);
    askConfirm({
      title: t("delete_sprint_confirm"),
      detail: sprint?.name,
      onConfirm: async () => {
        await window.api.sprints.delete(currentSprintId);
        setCurrentSprintId("");
        refresh();
      },
    });
  };

  const createStory = async (payload) => {
    await window.api.stories.create({
      boardId: currentBoardId,
      sprintId: currentSprintId || null,
      ...payload,
    });
    refresh();
    toast(t("save"), "success");
  };
  const editStory = async (payload) => {
    await window.api.stories.update(storyModal.initial.id, {
      ...payload,
      sprintId: storyModal.initial.sprintId ?? null,
    });
    refresh();
    toast(t("save"), "success");
  };
  const deleteStory = (story) => {
    askConfirm({
      title: t("delete_story_confirm"),
      detail: story.title,
      onConfirm: async () => {
        await window.api.stories.delete(story.id);
        refresh();
      },
    });
  };

  const createItem = async (payload) => {
    // Convert the human-readable type to its int value before posting (backend
    // expects WorkItemType as a number — see normalize* helpers at the top).
    const wirePayload = { ...payload, type: WORK_ITEM_TYPE_BY_NAME[payload.type] ?? 0 };
    await window.api.workItems.create(itemModal.story.id, wirePayload);
    refresh();
    toast(t("save"), "success");
  };
  const editItem = async (payload) => {
    await window.api.workItems.update(itemModal.initial.id, payload);
    refresh();
    toast(t("save"), "success");
  };
  const deleteItem = (item) => {
    askConfirm({
      title: t("delete_item_confirm"),
      detail: item.title,
      onConfirm: async () => {
        await window.api.workItems.delete(item.id);
        refresh();
      },
    });
  };

  // ── subTab-driven view derivation ──
  // - "boards"  → standard kanban (default)
  // - "mine"    → kanban filtered to items assigned to the current user; stories
  //               with zero matching items are hidden so the view stays focused
  // - "backlog" → kanban with sprintId forced to "" (handled by the effect above)
  // - "sprints" → switches the body to a list of sprints with metadata + actions
  const baseStories = view?.stories || [];
  const visibleStories = (subTab === "mine" && currentUserId)
    ? baseStories
        .map(s => ({ ...s, items: s.items.filter(i => i.assigneeTeamMemberId === currentUserId) }))
        .filter(s => s.items.length > 0)
    : baseStories;

  // ── Render ──
  if (loading && boards.length === 0) {
    return <div className="page"><window.PageHeader title={t("tasks_title")} subtitle={t("tasks_sub")} /><div className="empty">…</div></div>;
  }

  if (error) {
    return <div className="page"><window.PageHeader title={t("tasks_title")} /><div className="empty"><h3>Error</h3><p>{error}</p></div></div>;
  }

  if (boards.length === 0) {
    return (
      <div className="page">
        <window.PageHeader title={t("tasks_title")} subtitle={t("tasks_sub")} />
        <div className="empty" style={{ padding: "60px 24px" }}>
          <div className="empty-icon"><window.Icons.Kanban /></div>
          <h3>{t("no_boards")}</h3>
          <p>{t("no_boards_sub")}</p>
          <div style={{ marginTop: 12 }}>
            <window.Btn variant="brand" icon={window.Icons.Plus} onClick={() => setBoardModal({ open: true, initial: null })}>{t("new_board")}</window.Btn>
          </div>
        </div>
        <BoardModal open={boardModal.open} onClose={() => setBoardModal({ open: false, initial: null })} onSubmit={createBoard} t={t} initial={boardModal.initial} />
      </div>
    );
  }

  const boardOptions = boards.map(b => ({ value: b.id, label: b.name }));
  const sprintOptions = [
    { value: "", label: t("view_backlog") },
    ...sprints.map(s => ({ value: s.id, label: s.isActive ? `${s.name} · ${t("active_now")}` : s.name })),
  ];

  return (
    <div className="page">
      <window.PageHeader title={t("tasks_title")} subtitle={t("tasks_sub")} />

      <div className="kanban-toolbar">
        <div className="kanban-toolbar-group">
          <label className="kanban-toolbar-label">{t("board")}</label>
          <window.Select value={currentBoardId} onChange={(v) => { setCurrentBoardId(v); setCurrentSprintId(""); }} options={boardOptions} />
          <button className="btn sm" onClick={() => setBoardModal({ open: true, initial: null })} title={t("new_board")}><window.Icons.Plus /></button>
          <button className="btn sm" onClick={() => setBoardModal({ open: true, initial: currentBoard })} title={t("edit_board")} disabled={!currentBoard}><window.Icons.Edit /></button>
          <button className="btn sm" onClick={() => setBoardSettingsOpen(true)} title={t("board_settings")} disabled={!currentBoard}><window.Icons.Settings /></button>
          <button className="btn sm danger" onClick={deleteBoard} title={t("delete")} disabled={!currentBoard}><window.Icons.Trash /></button>
        </div>

        <div className="kanban-toolbar-group">
          <label className="kanban-toolbar-label">{t("select_sprint")}</label>
          <window.Select value={currentSprintId} onChange={setCurrentSprintId} options={sprintOptions} />
          <button className="btn sm" onClick={() => setSprintModal({ open: true, initial: null })} title={t("new_sprint")}><window.Icons.Plus /></button>
          <button className="btn sm" onClick={() => setSprintModal({ open: true, initial: sprints.find(s => s.id === currentSprintId) })} title={t("edit_sprint")} disabled={!currentSprintId}><window.Icons.Edit /></button>
          <button className="btn sm danger" onClick={deleteSprint} title={t("delete")} disabled={!currentSprintId}><window.Icons.Trash /></button>
        </div>

        <div className="kanban-toolbar-group" style={{ marginLeft: "auto" }}>
          <window.Btn variant="brand" icon={window.Icons.Plus} onClick={() => setStoryModal({ open: true, initial: null })}>{t("new_story")}</window.Btn>
        </div>
      </div>

      {subTab === "sprints" ? (
        <SprintsListView
          sprints={sprints}
          t={t}
          lang={lang}
          onEdit={(s) => setSprintModal({ open: true, initial: s })}
          onDelete={(s) => askConfirm({
            title: t("delete_sprint_confirm"),
            detail: s.name,
            onConfirm: async () => {
              await window.api.sprints.delete(s.id);
              if (currentSprintId === s.id) setCurrentSprintId("");
              refresh();
            },
          })}
          onOpen={(s) => { setCurrentSprintId(s.id); setSubTab && setSubTab("boards"); }}
        />
      ) : visibleStories.length === 0 ? (
        <div className="empty" style={{ marginTop: 12, padding: "40px 24px" }}>
          <div className="empty-icon"><window.Icons.CheckSquare /></div>
          <h3>{
            subTab === "mine" ? (lang === "es" ? "Nada asignado a ti aquí" : "Nothing assigned to you here") :
            currentSprintId ? t("no_stories_sprint") : t("no_stories_backlog")
          }</h3>
        </div>
      ) : (
        <div className="kanban-grid" style={{ gridTemplateColumns: `220px repeat(${states.length}, minmax(200px, 1fr))` }}>
          <div className="kanban-col-header" style={{ background: "transparent", borderBottom: "none" }}></div>
          {states.map(st => (
            <div key={st.id} className="kanban-col-header" data-initial={st.isInitial} data-terminal={st.isTerminal}>
              <span>{st.name}</span>
              <span className="kanban-col-count">{visibleStories.reduce((acc, s) => acc + s.items.filter(i => i.stateId === st.id).length, 0)}</span>
            </div>
          ))}

          {visibleStories.map(s => (
            <StoryRow
              key={s.story.id}
              story={s.story}
              items={s.items}
              states={states}
              members={members}
              t={t}
              onEditStory={() => setStoryModal({ open: true, initial: s.story })}
              onDeleteStory={() => deleteStory(s.story)}
              onAddItem={(story, type) => setItemModal({ open: true, initial: null, story, defaultType: type })}
              onEditItem={(it) => setItemModal({ open: true, initial: it, story: s.story, defaultType: it.type })}
              onDeleteItem={deleteItem}
              onMove={moveItem}
              dragState={dragState}
              setDragState={setDragState}
            />
          ))}
        </div>
      )}

      {view?.truncated && (
        <div className="kanban-truncated">
          {lang === "es"
            ? `Mostrando primeras ${visibleStories.length} stories. Filtra o crea un sprint para reducir la lista.`
            : `Showing first ${visibleStories.length} stories. Filter or create a sprint to narrow the list.`}
        </div>
      )}

      <BoardModal open={boardModal.open} onClose={() => setBoardModal({ open: false, initial: null })} onSubmit={boardModal.initial ? editBoard : createBoard} t={t} initial={boardModal.initial} />
      <SprintModal open={sprintModal.open} onClose={() => setSprintModal({ open: false, initial: null })} onSubmit={sprintModal.initial ? editSprint : createSprint} t={t} initial={sprintModal.initial} />
      <StoryModal open={storyModal.open} onClose={() => setStoryModal({ open: false, initial: null })} onSubmit={storyModal.initial ? editStory : createStory} t={t} initial={storyModal.initial} members={members} lang={lang} />
      <WorkItemModal
        open={itemModal.open}
        onClose={() => setItemModal({ open: false, initial: null, story: null, defaultType: "Task" })}
        onSubmit={itemModal.initial ? editItem : createItem}
        t={t}
        initial={itemModal.initial}
        defaultType={itemModal.defaultType}
        members={members}
      />
      <BoardSettingsModal
        open={boardSettingsOpen}
        onClose={() => setBoardSettingsOpen(false)}
        board={currentBoard}
        onChanged={refresh}
        t={t}
        lang={lang}
      />
      <ConfirmDialog state={confirmState} onClose={closeConfirm} t={t} />
    </div>
  );
};

Object.assign(window, { TasksScreen: window.TasksScreen });
