// calendar.jsx — month/week calendar view inside a project.
//
// Sits next to Kanban / Table tabs. Reads task.due strings (e.g.
// "May 12", "May 12 @ 3:00 PM", "Today", "Tomorrow") via the existing
// parseDateLoose helper from table.jsx, groups tasks by date, and
// renders them as colored chips on a 7-column month grid.
//
// Interactions:
//   - Click a chip          → onOpen(task) (opens drawer)
//   - Drag a chip → cell    → onUpdate(id, { due: newDateStr })  (preserves time)
//   - Click empty cell      → opens inline quick-add row
//   - Empty rail → cell     → schedules an unscheduled task by drag
//   - Hover chip            → tooltip with description
//
// Props (matches KanbanView signature so app.jsx wires it identically):
//   { tasks, onOpen, onUpdate, onAddTask, currentUserId, projectAccess, activeProjectId }
//
// `tasks` arrives already filtered by the parent (BoardToolbar filters
// flow into the same `filtered` array used by Kanban + Table).

const _MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const _MONTHS_FULL = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const _DOW = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];

// Social-media channels surfaced when a project has Content mode on.
// Each channel has a brand-ish color, a human label, and an inline
// SVG path (rendered white on the brand color in the chip pill).
// Add freely — the `id` is what gets persisted to tasks.channel.
//
// SVG paths are simplified single-path glyphs from the public-domain
// simple-icons project (CC0). All use viewBox="0 0 24 24" so the same
// renderer works for every channel.
const CHANNELS = [
  { id: "instagram", label: "Instagram",  short: "IG", color: "#E4405F",
    path: "M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z" },
  { id: "youtube", label: "YouTube", short: "YT", color: "#FF0000",
    path: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" },
  { id: "tiktok", label: "TikTok", short: "TT", color: "#000000",
    path: "M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z" },
  { id: "linkedin", label: "LinkedIn", short: "LI", color: "#0A66C2",
    path: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" },
  { id: "x", label: "X / Twitter", short: "X", color: "#0F1419",
    path: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" },
  { id: "facebook", label: "Facebook", short: "FB", color: "#1877F2",
    path: "M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647z" },
  { id: "blog", label: "Blog", short: "BL", color: "#3B82F6",
    path: "M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zM7 18a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0-7V8a8 8 0 0 1 8 8h-3a5 5 0 0 0-5-5zm5 7a8 8 0 0 0-5-5v-2a10 10 0 0 1 10 10h-2a3 3 0 0 0-3-3z" },
  { id: "newsletter", label: "Newsletter", short: "NL", color: "#10B981",
    path: "M22 6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6zm-2 0-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z" },
  { id: "podcast", label: "Podcast", short: "PC", color: "#7C3AED",
    path: "M12 14a3 3 0 0 0 3-3V5a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3zm5.91-3a1 1 0 0 0-2 0 4 4 0 0 1-7.82 0 1 1 0 0 0-2 0 6 6 0 0 0 5 5.91V20H8a1 1 0 0 0 0 2h8a1 1 0 0 0 0-2h-3v-2.09A6 6 0 0 0 17.91 11z" },
  { id: "threads", label: "Threads", short: "TH", color: "#000000",
    path: "M17.823 11.082c-.099-.046-.198-.09-.298-.131-.175-3.218-1.932-5.061-4.881-5.08-1.692-.011-3.099.7-3.965 2.005l1.622 1.113c.647-.982 1.663-1.191 2.354-1.191.012 0 .024 0 .036.001.86.005 1.51.255 1.93.74.305.354.51.844.604 1.464a10.83 10.83 0 0 0-2.473-.119c-2.487.143-4.087 1.594-3.979 3.611.054 1.024.564 1.904 1.435 2.478.736.485 1.683.722 2.668.668 1.302-.071 2.323-.567 3.036-1.475.541-.689.882-1.582 1.031-2.703.611.369 1.064.854 1.314 1.437.426.99.451 2.616-.876 3.943-1.163 1.162-2.561 1.665-4.674 1.681-2.343-.018-4.116-.769-5.27-2.235-1.081-1.371-1.64-3.353-1.66-5.892.02-2.539.578-4.521 1.66-5.892C7.51 3.74 9.282 2.99 11.625 2.972c2.359.018 4.16.772 5.357 2.246.587.722 1.029 1.629 1.32 2.689l1.92-.512c-.353-1.305-.913-2.42-1.677-3.342-1.5-1.847-3.694-2.798-6.523-2.819H11.99c-2.823.021-4.989.973-6.43 2.832C4.13 5.722 3.467 8.062 3.448 10.973v.014c.019 2.91.687 5.252 1.985 6.984 1.488 1.984 3.682 3.013 6.522 3.034h.034c2.55-.018 4.347-.685 5.797-2.135 1.892-1.892 1.835-4.295 1.18-5.731-.412-.926-1.16-1.694-2.143-2.057zm-3.523 4.087c-.97.054-1.997-.426-2.05-1.43-.039-.748.55-1.583 2.135-1.674a9.27 9.27 0 0 1 .597-.018 8.06 8.06 0 0 1 1.692.179c-.232 2.572-1.444 2.886-2.374 2.943z" },
];
const CHANNELS_BY_ID = Object.fromEntries(CHANNELS.map(c => [c.id, c]));
window.CHANNELS = CHANNELS;
window.CHANNELS_BY_ID = CHANNELS_BY_ID;

// Resolve the "anchor date" for a task — a real Date object so we can
// place it on the calendar grid. Handles the literal strings the rest
// of the app uses ("Today", "Tomorrow") plus the standard "Apr 12"
// formats parseDateLoose understands.
function _taskAnchorDate(task) {
  const v = task && task.due;
  if (!v || v === "—") return null;
  const t = new Date();
  if (v === "Today") return new Date(t.getFullYear(), t.getMonth(), t.getDate());
  if (v === "Tomorrow") return new Date(t.getFullYear(), t.getMonth(), t.getDate() + 1);
  if (typeof window !== "undefined" && typeof window.parseDateLoose === "function") {
    return window.parseDateLoose(v);
  }
  return null;
}

// Strip time portion (if any) and return the bare "Mon DD" date part
// so we can re-attach a fresh date later.
function _taskTimePart(task) {
  if (typeof window === "undefined" || typeof window.parseDueTime !== "function") return null;
  return window.parseDueTime(task && task.due);
}

function _dateKey(d) {
  return d ? `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}` : "";
}
function _isSameDay(a, b) {
  return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}

// Build the 6-row x 7-col cell grid for a given month, including
// trailing/leading days from neighbouring months so every row is full.
function _monthCells(year, month) {
  const first = new Date(year, month, 1);
  const dow = first.getDay();
  const start = new Date(year, month, 1 - dow);
  const cells = [];
  for (let i = 0; i < 42; i++) {
    const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
    cells.push(d);
  }
  return cells;
}

// Group tasks by the cell date key. Tasks without a date go into "" bucket.
function _bucketByDate(tasks) {
  const map = new Map();
  (tasks || []).forEach(t => {
    const d = _taskAnchorDate(t);
    const k = d ? _dateKey(d) : "";
    if (!map.has(k)) map.set(k, []);
    map.get(k).push(t);
  });
  // Sort within each bucket: time ascending, then priority, then name
  const PRIO = { critical: 0, high: 1, medium: 2, low: 3, none: 4 };
  for (const arr of map.values()) {
    arr.sort((a, b) => {
      const ta = _taskTimePart(a); const tb = _taskTimePart(b);
      const av = ta ? ta.hh * 60 + ta.mm : 9999;
      const bv = tb ? tb.hh * 60 + tb.mm : 9999;
      if (av !== bv) return av - bv;
      const pa = PRIO[a.prio] ?? 5; const pb = PRIO[b.prio] ?? 5;
      if (pa !== pb) return pa - pb;
      return String(a.name).localeCompare(String(b.name));
    });
  }
  return map;
}

// Format a Date back into the project's display format. Re-uses
// formatDateShort + formatDueWithTime from table.jsx so we stay
// consistent with the rest of the app.
//
// When the target date is in a year *other than* the current year we
// append ", YYYY" so the round-trip through parseDateLoose is lossless
// — without it, "Jan 05" written in December would be mis-read as the
// past January when re-rendered. parseDateLoose already accepts the
// optional ", YYYY" suffix, so the rest of the app keeps working.
function _formatDateForApi(d, time) {
  if (!d) return null;
  const currentYear = new Date().getFullYear();
  const includeYear = d.getFullYear() !== currentYear;
  const base = (typeof window !== "undefined" && typeof window.formatDateShort === "function")
    ? window.formatDateShort(d)
    : `${_MONTHS[d.getMonth()]} ${d.getDate()}`;
  const dateStr = includeYear ? `${base}, ${d.getFullYear()}` : base;
  if (time && time.hh != null) {
    const fmt12 = (typeof window !== "undefined" && typeof window._fmt12 === "function")
      ? window._fmt12
      : (hh, mm) => {
          const ampm = hh >= 12 ? "PM" : "AM";
          const h12 = ((hh + 11) % 12) + 1;
          return `${h12}:${String(mm).padStart(2,"0")} ${ampm}`;
        };
    return `${dateStr} @ ${fmt12(time.hh, time.mm)}`;
  }
  return dateStr;
}

// ── Channel pill (small brand-icon circle) ─────────────────────────
function ChannelPill({ id, size = "sm" }) {
  const ch = CHANNELS_BY_ID[id];
  if (!ch) return null;
  const dim = size === "lg" ? 22 : 16;
  // Inset the SVG slightly so the brand glyph sits centred inside
  // the colored circle without touching its edges.
  const iconSize = size === "lg" ? 14 : 10;
  return (
    <span
      className="cal-channel-pill"
      title={ch.label}
      style={{
        background: ch.color,
        color: "#fff",
        width: dim,
        height: dim,
        borderRadius: 999,
        display: "inline-flex",
        alignItems: "center",
        justifyContent: "center",
        flexShrink: 0,
      }}
    >
      {ch.path
        ? (
          <svg
            viewBox="0 0 24 24"
            width={iconSize}
            height={iconSize}
            fill="currentColor"
            aria-hidden="true"
            focusable="false"
          >
            <path d={ch.path}/>
          </svg>
        )
        : <span style={{ fontSize: size === "lg" ? 10 : 8.5, fontWeight: 800 }}>{ch.short}</span>}
    </span>
  );
}

// ── Event chip (a task on the calendar) ────────────────────────────
function CalEvent({ task, onOpen, onDragStart, isDragging, contentMode = false, compact = false }) {
  const time = _taskTimePart(task);
  const overdue = typeof window !== "undefined" && typeof window.isOverdueNow === "function"
    ? window.isOverdueNow(task) : false;
  const channel = contentMode ? (task.channel || null) : null;
  const channelMeta = channel ? CHANNELS_BY_ID[channel] : null;
  const cls =
    "cal-event prio-" + (task.prio || "none") +
    (task.status === "done" ? " is-done" : "") +
    (overdue && task.status !== "done" ? " is-overdue" : "") +
    (channelMeta ? " has-channel" : "") +
    (isDragging ? " is-dragging" : "");
  const owner = (Array.isArray(task.owners) && task.owners[0])
    ? (typeof PEOPLE !== "undefined" ? PEOPLE.find(p => p.id === task.owners[0]) : null)
    : null;
  const ownerInitials = owner
    ? String(owner.name).split(/\s+/).map(s => s[0]).slice(0,2).join("").toUpperCase()
    : null;
  const timeStr = time ? (
    typeof window !== "undefined" && typeof window._fmt12 === "function"
      ? window._fmt12(time.hh, time.mm)
      : `${((time.hh + 11) % 12) + 1}:${String(time.mm).padStart(2,"0")} ${time.hh >= 12 ? "PM" : "AM"}`
  ) : null;
  // When a channel is set, override the priority side-stripe with the
  // channel color so it scans as "this is an Instagram post" rather
  // than "this is high priority".
  const styleOverride = channelMeta ? { borderLeftColor: channelMeta.color } : {};
  return (
    <div
      className={cls}
      style={styleOverride}
      title={(channelMeta ? `[${channelMeta.label}] ` : "") + task.name +
             (task.desc ? "\n\n" + String(task.desc).replace(/<[^>]+>/g, "").slice(0, 280) : "")}
      draggable
      onDragStart={(e) => {
        e.dataTransfer.effectAllowed = "move";
        e.dataTransfer.setData("text/plain", task.id);
        onDragStart && onDragStart(task);
      }}
      onClick={(e) => { e.stopPropagation(); onOpen && onOpen(task); }}
    >
      {channelMeta && <ChannelPill id={channel}/>}
      {timeStr && <span className="cal-event-time">{timeStr}</span>}
      <span className="cal-event-title">{task.name}</span>
      {!compact && ownerInitials && (
        <span className="cal-event-avatar" title={owner ? owner.name : ""}>
          {owner && owner.avatar ? <img src={owner.avatar} alt=""/> : ownerInitials}
        </span>
      )}
    </div>
  );
}

// ── Compact chip used in the side-rail (Unscheduled, Overdue) ─────
function CalRailEvent({ task, onOpen, onDragStart, meta }) {
  return (
    <div
      className={"cal-rail-event prio-" + (task.prio || "none")}
      draggable
      onDragStart={(e) => {
        e.dataTransfer.effectAllowed = "move";
        e.dataTransfer.setData("text/plain", task.id);
        onDragStart && onDragStart(task);
      }}
      onClick={() => onOpen && onOpen(task)}
      title={task.name}
    >
      <span className="cal-event-title">{task.name}</span>
      {meta && <span className="cal-rail-event-meta">{meta}</span>}
    </div>
  );
}

// ── Quick-add inline row (lives inside a popover near the cell) ───
function CalQuickAdd({ date, onSubmit, onCancel }) {
  const [name, setName] = React.useState("");
  const inputRef = React.useRef(null);
  React.useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []);
  const dateLabel = date
    ? `${_MONTHS_FULL[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`
    : "No date";
  function go() {
    const trimmed = name.trim();
    if (!trimmed) return;
    onSubmit(trimmed);
  }
  return (
    <div className="cal-quick" onClick={(e) => e.stopPropagation()}>
      <div className="cal-quick-date">{dateLabel}</div>
      <input
        ref={inputRef}
        className="cal-quick-input"
        placeholder="New task name…"
        value={name}
        onChange={e => setName(e.target.value)}
        onKeyDown={e => {
          if (e.key === "Enter") go();
          if (e.key === "Escape") onCancel();
        }}
      />
      <div className="cal-quick-actions">
        <button className="cal-quick-btn" onClick={onCancel}>Cancel</button>
        <button className="cal-quick-btn is-primary" onClick={go} disabled={!name.trim()}>Add</button>
      </div>
    </div>
  );
}

// ── Main component ────────────────────────────────────────────────
function CalendarView({ tasks, onOpen, onUpdate, onAddTask, currentUserId, projectAccess, activeProjectId }) {
  const today = React.useMemo(() => {
    const t = new Date();
    return new Date(t.getFullYear(), t.getMonth(), t.getDate());
  }, []);
  const [cursor, setCursor] = React.useState(() => new Date(today.getFullYear(), today.getMonth(), 1));
  const [mode, setMode] = React.useState("month");           // "month" | "week"
  const [draggingId, setDraggingId] = React.useState(null);
  const [dropKey, setDropKey] = React.useState(null);
  const [quick, setQuick] = React.useState(null);            // { date, anchor: {x,y} }
  // Channel filter — null means "all channels". Persists per-project
  // in localStorage so it survives reloads.
  const filterStorageKey = `fb.cal.channelFilter.${activeProjectId || "global"}`;
  const [channelFilter, setChannelFilter] = React.useState(() => {
    try { return localStorage.getItem(filterStorageKey) || null; } catch { return null; }
  });
  const [channelMenuOpen, setChannelMenuOpen] = React.useState(false);
  // Read the current project's content-mode flag lazily so toggling
  // it via the API + mutating the global PROJECTS array re-renders
  // immediately. We also keep a local "tick" state to force a refresh
  // after we mutate PROJECTS in place.
  const [, _bumpTick] = React.useState(0);
  const activeProject = (typeof PROJECTS !== "undefined")
    ? PROJECTS.find(p => p.id === activeProjectId) || null
    : null;
  const contentMode = !!(activeProject && activeProject.isContentCalendar);

  // Apply the channel filter to the visible task set. When content
  // mode is off the filter is a no-op (channels are hidden anyway).
  const filteredTasks = React.useMemo(() => {
    if (!contentMode || !channelFilter) return tasks;
    return (tasks || []).filter(t => (t.channel || null) === channelFilter);
  }, [tasks, contentMode, channelFilter]);

  const buckets = React.useMemo(() => _bucketByDate(filteredTasks), [filteredTasks]);
  const unscheduled = buckets.get("") || [];

  function setChannelFilterPersist(next) {
    setChannelFilter(next);
    try {
      if (next) localStorage.setItem(filterStorageKey, next);
      else localStorage.removeItem(filterStorageKey);
    } catch {}
  }

  // Re-render whenever Project settings flips the content-mode flag.
  // The settings modal mutates PROJECTS in place, then dispatches
  // `flowboard:project:patched` which we listen for here.
  React.useEffect(() => {
    function onPatched(e) {
      const id = e && e.detail && e.detail.id;
      if (id === activeProjectId) _bumpTick(n => n + 1);
    }
    window.addEventListener("flowboard:project:patched", onPatched);
    return () => window.removeEventListener("flowboard:project:patched", onPatched);
  }, [activeProjectId]);

  // Overdue rail content — anything with a real anchor date strictly
  // before today and status != done.
  const overdue = React.useMemo(() => {
    const out = [];
    (tasks || []).forEach(t => {
      if (t.status === "done") return;
      const d = _taskAnchorDate(t);
      if (!d) return;
      if (d < today) out.push({ task: t, date: d });
    });
    out.sort((a, b) => a.date - b.date);
    return out;
  }, [tasks, today]);

  function shiftMonth(delta) {
    setCursor(c => new Date(c.getFullYear(), c.getMonth() + delta, 1));
  }
  function shiftWeek(delta) {
    setCursor(c => new Date(c.getFullYear(), c.getMonth(), c.getDate() + delta * 7));
  }
  function gotoToday() {
    setCursor(mode === "week"
      ? new Date(today.getFullYear(), today.getMonth(), today.getDate())
      : new Date(today.getFullYear(), today.getMonth(), 1));
  }

  // ── Drag & drop ───────────────────────────────────────────────
  function handleDragStart(task) { setDraggingId(task.id); setDropKey(null); }
  function handleDragOverCell(e, key) {
    if (!draggingId) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
    if (dropKey !== key) setDropKey(key);
  }
  function handleDropOnCell(e, date) {
    e.preventDefault();
    const id = draggingId || (e.dataTransfer && e.dataTransfer.getData("text/plain"));
    setDraggingId(null);
    setDropKey(null);
    if (!id || !date) return;
    const task = (tasks || []).find(t => t.id === id);
    if (!task) return;
    const time = _taskTimePart(task);
    const newDue = _formatDateForApi(date, time);
    if (!newDue || newDue === task.due) return;
    onUpdate && onUpdate(id, { due: newDue });
  }
  function handleDragEnd() { setDraggingId(null); setDropKey(null); }

  // ── Quick-add ────────────────────────────────────────────────
  function openQuickAdd(date, ev) {
    if (ev && ev.target && ev.target.closest && ev.target.closest(".cal-event")) return;
    const r = ev && ev.currentTarget ? ev.currentTarget.getBoundingClientRect() : null;
    setQuick({
      date,
      anchor: r ? { x: r.left + r.width / 2, y: r.bottom + 6 } : null,
    });
  }
  function submitQuick(name) {
    if (!quick) return;
    const due = _formatDateForApi(quick.date, null);
    onAddTask && onAddTask({
      name,
      due,
      sprint: null,
      epicId: null,
      type: "task",
      prio: "medium",
      status: "todo",
      points: 3,
      owners: [],
      desc: "",
      subtasks: 0,
    });
    setQuick(null);
  }

  // Close the quick-add when clicking outside
  React.useEffect(() => {
    if (!quick) return;
    function onDoc(e) {
      const el = document.querySelector(".cal-quick-popover");
      if (!el) return;
      if (!el.contains(e.target)) setQuick(null);
    }
    function onKey(e) { if (e.key === "Escape") setQuick(null); }
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onKey);
    };
  }, [quick]);

  // ── Title for the head ───────────────────────────────────────
  const headTitle = mode === "week"
    ? (() => {
        const start = new Date(cursor); start.setDate(cursor.getDate() - cursor.getDay());
        const end   = new Date(start);  end.setDate(start.getDate() + 6);
        const sm = _MONTHS_FULL[start.getMonth()], em = _MONTHS_FULL[end.getMonth()];
        if (sm === em) return `${sm} ${start.getDate()}–${end.getDate()}, ${start.getFullYear()}`;
        return `${_MONTHS[start.getMonth()]} ${start.getDate()} – ${_MONTHS[end.getMonth()]} ${end.getDate()}, ${end.getFullYear()}`;
      })()
    : `${_MONTHS_FULL[cursor.getMonth()]} ${cursor.getFullYear()}`;

  // Count of tasks visibly scheduled in the current viewport
  const visibleCount = React.useMemo(() => {
    if (mode === "week") {
      const start = new Date(cursor); start.setDate(cursor.getDate() - cursor.getDay());
      let c = 0;
      for (let i = 0; i < 7; i++) {
        const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
        c += (buckets.get(_dateKey(d)) || []).length;
      }
      return c;
    }
    let c = 0;
    const cells = _monthCells(cursor.getFullYear(), cursor.getMonth());
    cells.forEach(d => { if (d.getMonth() === cursor.getMonth()) c += (buckets.get(_dateKey(d)) || []).length; });
    return c;
  }, [buckets, cursor, mode]);

  return (
    <div className="cal-wrap" onDragEnd={handleDragEnd}>
      <div className="cal-main">
        <div className="cal-head">
          {/* Title block — single visual unit, doesn't fight with buttons. */}
          <div className="cal-head-title">
            <div className="cal-title">{headTitle}</div>
            <div className="cal-sub">
              {visibleCount} task{visibleCount === 1 ? "" : "s"} scheduled
            </div>
          </div>

          {/* Right-aligned control cluster — nav, today, channel
              filter, mode toggle. All same height so they line up. */}
          <div className="cal-head-controls">
            <div className="cal-nav">
              <button className="cal-nav-btn"
                      onClick={() => mode === "week" ? shiftWeek(-1) : shiftMonth(-1)}
                      aria-label="Previous">‹</button>
              <button className="cal-nav-btn"
                      onClick={() => mode === "week" ? shiftWeek(1)  : shiftMonth(1)}
                      aria-label="Next">›</button>
            </div>
            <button className="cal-today-btn" onClick={gotoToday}>Today</button>

            {/* Channel filter — only when project's content mode is on */}
            {contentMode && (
              <div style={{ position: "relative" }}>
                <button
                  className="cal-today-btn"
                  onClick={() => setChannelMenuOpen(o => !o)}
                  style={channelFilter ? {
                    background: CHANNELS_BY_ID[channelFilter]?.color,
                    borderColor: CHANNELS_BY_ID[channelFilter]?.color,
                    color: "#fff",
                  } : null}
                  title="Filter by channel"
                >
                  {channelFilter
                    ? <>{CHANNELS_BY_ID[channelFilter]?.short || "?"} · {CHANNELS_BY_ID[channelFilter]?.label || channelFilter}</>
                    : <>All channels ▾</>}
                </button>
                {channelMenuOpen && (
                  <div
                    className="popover"
                    style={{
                      position: "absolute",
                      top: "calc(100% + 6px)",
                      right: 0,
                      minWidth: 200,
                      padding: 6,
                      zIndex: 10100,
                    }}
                    onMouseDown={e => e.stopPropagation()}
                  >
                    <div
                      className="popover-item"
                      style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 10px", cursor: "pointer" }}
                      onClick={() => { setChannelFilterPersist(null); setChannelMenuOpen(false); }}
                    >
                      <span style={{ display: "inline-block", width: 16, height: 16, borderRadius: 999, background: "#e6e9ef" }}/>
                      All channels
                    </div>
                    {CHANNELS.map(c => (
                      <div
                        key={c.id}
                        className="popover-item"
                        style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 10px", cursor: "pointer" }}
                        onClick={() => { setChannelFilterPersist(c.id); setChannelMenuOpen(false); }}
                      >
                        <ChannelPill id={c.id}/>
                        {c.label}
                      </div>
                    ))}
                  </div>
                )}
              </div>
            )}

            <div className="cal-modes">
              <button className={"cal-mode" + (mode === "month" ? " is-active" : "")}
                      onClick={() => setMode("month")}>Month</button>
              <button className={"cal-mode" + (mode === "week"  ? " is-active" : "")}
                      onClick={() => setMode("week")}>Week</button>
            </div>
          </div>
        </div>

        {mode === "month" ? (
          <CalMonth
            cursor={cursor}
            today={today}
            buckets={buckets}
            onOpen={onOpen}
            onDragStart={handleDragStart}
            onDragOverCell={handleDragOverCell}
            onDropOnCell={handleDropOnCell}
            dropKey={dropKey}
            draggingId={draggingId}
            onCellClick={openQuickAdd}
            contentMode={contentMode}
          />
        ) : (
          <CalWeek
            cursor={cursor}
            today={today}
            buckets={buckets}
            onOpen={onOpen}
            onDragStart={handleDragStart}
            onDragOverCell={handleDragOverCell}
            onDropOnCell={handleDropOnCell}
            dropKey={dropKey}
            draggingId={draggingId}
            onCellClick={openQuickAdd}
            contentMode={contentMode}
          />
        )}
      </div>

      <div className="cal-rail">
        <div className="cal-rail-card">
          <div className="cal-rail-head">
            <div className="cal-rail-title">
              <Icons.Inbox/> Unscheduled
            </div>
            <div className="cal-rail-count">{unscheduled.length}</div>
          </div>
          <div className="cal-rail-list">
            {unscheduled.length === 0
              ? <div className="cal-rail-empty">Everything's scheduled. Nice.</div>
              : unscheduled.slice(0, 50).map(t => (
                  <CalRailEvent key={t.id} task={t} onOpen={onOpen} onDragStart={handleDragStart}/>
                ))
            }
          </div>
        </div>

        <div className="cal-rail-card">
          <div className="cal-rail-head">
            <div className="cal-rail-title">
              <Icons.Clock/> Overdue
            </div>
            <div className="cal-rail-count">{overdue.length}</div>
          </div>
          <div className="cal-rail-list">
            {overdue.length === 0
              ? <div className="cal-rail-empty">No overdue tasks.</div>
              : overdue.slice(0, 50).map(({ task, date }) => {
                  const days = Math.floor((today - date) / 86400000);
                  return (
                    <CalRailEvent
                      key={task.id}
                      task={task}
                      onOpen={onOpen}
                      onDragStart={handleDragStart}
                      meta={days <= 0 ? "today" : `${days}d ago`}
                    />
                  );
                })
            }
          </div>
        </div>
      </div>

      {quick && quick.anchor && (
        <div
          className="popover cal-quick-popover"
          style={{
            position: "fixed",
            left: Math.min(window.innerWidth - 300, Math.max(8, quick.anchor.x - 140)),
            top: Math.min(window.innerHeight - 220, quick.anchor.y),
            zIndex: 10100,
          }}
        >
          <CalQuickAdd date={quick.date} onSubmit={submitQuick} onCancel={() => setQuick(null)}/>
        </div>
      )}
    </div>
  );
}

// ── Month grid (6 rows of 7 cells) ────────────────────────────────
function CalMonth({ cursor, today, buckets, onOpen, onDragStart, onDragOverCell, onDropOnCell, dropKey, draggingId, onCellClick, contentMode = false }) {
  const cells = React.useMemo(
    () => _monthCells(cursor.getFullYear(), cursor.getMonth()),
    [cursor]
  );
  const month = cursor.getMonth();
  const MAX_CHIPS = 4;

  return (
    <>
      <div className="cal-dow">
        {_DOW.map((d, i) => (
          <div key={d} className={i === 0 || i === 6 ? "is-weekend" : ""}>{d}</div>
        ))}
      </div>
      <div className="cal-grid">
        {cells.map((d, i) => {
          const key = _dateKey(d);
          const inMonth = d.getMonth() === month;
          const isToday = _isSameDay(d, today);
          const isPast = d < today && !isToday;
          const dow = d.getDay();
          const list = buckets.get(key) || [];
          const overflow = Math.max(0, list.length - MAX_CHIPS);
          const cls = [
            "cal-cell",
            inMonth ? "" : "is-other-month",
            (dow === 0 || dow === 6) ? "is-weekend" : "",
            isToday ? "is-today" : "",
            (isPast && !isToday) ? "is-past" : "",
            (dropKey === key) ? "is-drop-target" : "",
          ].filter(Boolean).join(" ");
          return (
            <div
              key={i}
              className={cls}
              onDragOver={(e) => onDragOverCell(e, key)}
              onDrop={(e) => onDropOnCell(e, d)}
              onClick={(e) => onCellClick(d, e)}
            >
              <div className="cal-date">{isToday ? `${d.getDate()} · Today` : d.getDate()}</div>
              <button
                className="cal-add"
                title="Add task on this day"
                onClick={(e) => { e.stopPropagation(); onCellClick(d, { currentTarget: e.currentTarget.parentElement, target: e.currentTarget }); }}
              >+</button>
              {list.slice(0, MAX_CHIPS).map(t => (
                <CalEvent
                  key={t.id}
                  task={t}
                  onOpen={onOpen}
                  onDragStart={onDragStart}
                  isDragging={draggingId === t.id}
                  contentMode={contentMode}
                />
              ))}
              {overflow > 0 && (
                <div
                  className="cal-more"
                  onClick={(e) => {
                    e.stopPropagation();
                    // Open the first overflow task as a hint — full
                    // overflow popover is a follow-up enhancement.
                    onOpen && onOpen(list[MAX_CHIPS]);
                  }}
                >+{overflow} more</div>
              )}
            </div>
          );
        })}
      </div>
    </>
  );
}

// ── Week strip (7 columns, scrollable) ────────────────────────────
function CalWeek({ cursor, today, buckets, onOpen, onDragStart, onDragOverCell, onDropOnCell, dropKey, draggingId, onCellClick, contentMode = false }) {
  const weekStart = React.useMemo(() => {
    const s = new Date(cursor);
    s.setDate(cursor.getDate() - cursor.getDay());
    return new Date(s.getFullYear(), s.getMonth(), s.getDate());
  }, [cursor]);
  const days = React.useMemo(() => {
    const arr = [];
    for (let i = 0; i < 7; i++) {
      const d = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + i);
      arr.push(d);
    }
    return arr;
  }, [weekStart]);

  return (
    <>
      <div className="cal-dow" style={{ gridTemplateColumns: "repeat(7, minmax(0, 1fr))" }}>
        {days.map((d, i) => {
          const isToday = _isSameDay(d, today);
          const dow = d.getDay();
          return (
            <div
              key={i}
              className={"cal-week-day-header" + (isToday ? " is-today" : "") + ((dow === 0 || dow === 6) ? " is-weekend" : "")}
              style={{ background: "transparent", border: 0, padding: "10px 4px", textAlign: "center" }}
            >
              {_DOW[d.getDay()]}
              <span className="num">{d.getDate()}</span>
            </div>
          );
        })}
      </div>
      <div className="cal-grid" style={{ gridTemplateColumns: "repeat(7, minmax(0, 1fr))", gridAutoRows: "1fr", minHeight: 460 }}>
        {days.map((d, i) => {
          const key = _dateKey(d);
          const isToday = _isSameDay(d, today);
          const list = buckets.get(key) || [];
          const cls = [
            "cal-cell",
            isToday ? "is-today" : "",
            (dropKey === key) ? "is-drop-target" : "",
          ].filter(Boolean).join(" ");
          return (
            <div
              key={i}
              className={cls}
              onDragOver={(e) => onDragOverCell(e, key)}
              onDrop={(e) => onDropOnCell(e, d)}
              onClick={(e) => onCellClick(d, e)}
              style={{ minHeight: 460, paddingTop: 10 }}
            >
              <button
                className="cal-add"
                title="Add task on this day"
                onClick={(e) => { e.stopPropagation(); onCellClick(d, { currentTarget: e.currentTarget.parentElement, target: e.currentTarget }); }}
              >+</button>
              {list.length === 0
                ? null
                : list.map(t => (
                    <CalEvent
                      key={t.id}
                      task={t}
                      onOpen={onOpen}
                      onDragStart={onDragStart}
                      isDragging={draggingId === t.id}
                      contentMode={contentMode}
                    />
                  ))
              }
            </div>
          );
        })}
      </div>
    </>
  );
}

Object.assign(window, { CalendarView, CalEvent, CalRailEvent, ChannelPill });
