function createPlanToday({ identity, save, start, limit = 5 }) {
  let openState = false;
  let draftIds = [];
  let itemsById = new Map();
  let capacityMinutes = null;
  let estimates = {};
  let recommendations = {};
  let recommendationAware = false;
  let capacityAware = false;
  let buildState = null;
  let buildReadiness = null;

  function cleanItems(items) {
    const unique = new Map();
    for (const item of items || []) {
      const id = identity?.(item);
      if (id && !unique.has(id)) unique.set(id, item);
    }
    return unique;
  }

  function open(selectedItems, candidates, planning = null, actualMinutes = null) {
    itemsById = cleanItems([...(selectedItems || []), ...(candidates || [])]);
    draftIds = [];
    for (const item of selectedItems || []) {
      const id = identity?.(item);
      if (id && itemsById.has(id) && !draftIds.includes(id) && draftIds.length < limit) draftIds.push(id);
    }
    capacityAware = Boolean(planning && ('capacity_minutes' in planning || 'estimates' in planning));
    capacityMinutes = Number.isInteger(planning?.capacity_minutes) && planning.capacity_minutes > 0
      ? planning.capacity_minutes : null;
    estimates = {};
    for (const [id, minutes] of Object.entries(planning?.estimates || {})) {
      if (Number.isInteger(minutes) && minutes > 0) estimates[id] = minutes;
    }
    recommendations = {};
    recommendationAware = actualMinutes !== null;
    buildState = null;
    buildReadiness = null;
    for (const [id, minutes] of Object.entries(actualMinutes || {})) {
      if (draftIds.includes(id) && Number.isInteger(minutes) && minutes >= 5 && minutes <= 1440 &&
          estimates[id] !== minutes) recommendations[id] = minutes;
    }
    if (Object.keys(recommendations).length) capacityAware = true;
    openState = true;
    return snapshot();
  }

  function toggle(item) {
    if (!openState) return 'closed';
    const id = identity?.(item);
    if (!id) return 'unavailable';
    itemsById.set(id, item);
    const index = draftIds.indexOf(id);
    if (index >= 0) {
      draftIds.splice(index, 1);
      return 'removed';
    }
    if (draftIds.length >= limit) return 'full';
    draftIds.push(id);
    return 'added';
  }

  function move(id, direction) {
    const index = draftIds.indexOf(id);
    const target = direction === 'up' ? index - 1 : direction === 'down' ? index + 1 : -1;
    if (!openState || index < 0 || target < 0 || target >= draftIds.length) return false;
    [draftIds[index], draftIds[target]] = [draftIds[target], draftIds[index]];
    return true;
  }

  function close() {
    openState = false;
    draftIds = [];
    itemsById = new Map();
    capacityMinutes = null;
    estimates = {};
    recommendations = {};
    recommendationAware = false;
    capacityAware = false;
    buildState = null;
    buildReadiness = null;
  }

  function cancel() {
    close();
    return true;
  }

  function setCapacity(minutes) {
    capacityAware = true;
    capacityMinutes = Number.isInteger(minutes) && minutes > 0 ? minutes : null;
    return snapshot();
  }

  function setEstimate(id, minutes) {
    const resolvingBuildEstimate = buildState?.needs_estimate?.includes(id);
    if (!openState || (!draftIds.includes(id) && !resolvingBuildEstimate)) return false;
    capacityAware = true;
    if (!Number.isInteger(minutes) || minutes < 5 || minutes > 1440) return false;
    estimates[id] = minutes;
    if (resolvingBuildEstimate && buildReadiness) buildRecommendation(buildReadiness);
    return true;
  }

  function applyRecommendation(id) {
    const minutes = recommendations[id];
    if (!openState || !draftIds.includes(id) || !Number.isInteger(minutes)) return false;
    estimates[id] = minutes;
    delete recommendations[id];
    capacityAware = true;
    return true;
  }

  function buildRecommendation(readiness = {}) {
    if (!openState) return null;
    buildReadiness = { ...readiness };
    const selected = [];
    const reasons = {};
    const skipped = [];
    const needsEstimate = [];
    let used = 0;
    for (const [id, item] of itemsById) {
      if (selected.length >= limit) break;
      const check = readiness[id] || { status:'unverified' };
      if (check.status !== 'ready') {
        skipped.push({
          id,
          status:check.status === 'blocked' ? 'blocked' : 'unverified',
          reason:check.reason || (check.status === 'blocked' ? 'Blocked' : 'Could not verify readiness'),
        });
        continue;
      }
      const minutes = estimates[id];
      if (!Number.isInteger(minutes)) {
        needsEstimate.push(id);
        continue;
      }
      if (capacityMinutes !== null && used + minutes > capacityMinutes) continue;
      selected.push(id);
      reasons[id] = item.reason || item.attention_reason || 'Ranked My Work';
      used += minutes;
    }
    draftIds = selected;
    capacityAware = true;
    buildState = { selected:[...selected], reasons, skipped, needs_estimate:needsEstimate };
    return buildState;
  }

  function commit({ start: startAfterSave = false, confirmOverCapacity = false } = {}) {
    if (!openState) return 'closed';
    const ids = [...draftIds];
    const state = snapshot();
    if (state.over_capacity && !confirmOverCapacity) return 'confirm-over-capacity';
    const payload = capacityAware ? {
      ids,
      capacity_minutes: capacityMinutes,
      estimates: Object.fromEntries(ids.filter(id => estimates[id]).map(id => [id, estimates[id]])),
    } : ids;
    if (save?.(payload) === false) return 'unavailable';
    const first = ids.length ? itemsById.get(ids[0]) : null;
    close();
    if (startAfterSave && first) start?.(first);
    return 'saved';
  }

  function snapshot() {
    const basic = { open: openState, ids: [...draftIds], count: draftIds.length, limit };
    if (!capacityAware) return basic;
    const selectedEstimates = Object.fromEntries(draftIds.filter(id => estimates[id]).map(id => [id, estimates[id]]));
    const plannedMinutes = Object.values(selectedEstimates).reduce((total, value) => total + value, 0);
    const remainingMinutes = capacityMinutes === null ? null : capacityMinutes - plannedMinutes;
    return {
      ...basic,
      capacity_minutes: capacityMinutes,
      estimates: selectedEstimates,
      ...(recommendationAware ? { recommendations:Object.fromEntries(
        draftIds.filter(id => recommendations[id]).map(id => [id, recommendations[id]])
      ) } : {}),
      planned_minutes: plannedMinutes,
      remaining_minutes: remainingMinutes,
      unestimated_count: draftIds.length - Object.keys(selectedEstimates).length,
      over_capacity: remainingMinutes !== null && remainingMinutes < 0,
      ...(buildState ? { build:buildState } : {}),
    };
  }

  function item(id) {
    return itemsById.get(id) || null;
  }

  function candidates() {
    return [...itemsById.entries()].filter(([id]) => !draftIds.includes(id)).map(([, value]) => value);
  }

  return { open, toggle, move, cancel, setCapacity, setEstimate, applyRecommendation, buildRecommendation, commit, snapshot, item, candidates };
}

if (typeof module !== 'undefined' && module.exports) module.exports = createPlanToday;
