/* Month view — 6-week grid, multi-day spans, drag-to-reschedule, sub-tasks as
   indented child pills under their parent on their scheduled date.
*/

const { useState: useStateM, useMemo: useMemoM } = React;
const UM = window.mcalUtils;

function MonthView({ cursor, today, events, options, colorBy, weeks, openEvent, addEventAt, update, nestAsSubtask, liftSubtask, moveSubtask }) {
  // Two modes share this view:
  //   weeks omitted (or 6) → full month grid anchored to the 1st
  //   weeks = 2            → fortnight grid anchored to the Sunday of
  //                           the week containing the cursor
  // In fortnight mode every off-month dimming + "today" handling still
  // works thanks to currentMonth + per-cell isToday checks below.
  const fortnight = weeks === 2;
  const weekCount = fortnight ? 2 : 6;
  const gridStart = fortnight
    ? UM.weekStartSunday(cursor)
    : UM.weekStartSunday(UM.monthStart(cursor));
  const weekRows = useMemoM(() => {
    const w = [];
    for (let i = 0; i < weekCount; i++) {
      const s = UM.addDays(gridStart, i * 7);
      w.push({ start: s, end: UM.addDays(s, 6) });
    }
    return w;
  }, [gridStart, weekCount]);
  const currentMonth = UM.parseISO(cursor).getUTCMonth();
  const [draggingId, setDraggingId] = useStateM(null);
  const [dragHoverDate, setDragHoverDate] = useStateM(null);

  const onDragStart = (e, ev) => {
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/plain', ev.id);
    setDraggingId(ev.id);
    // Hide any hover-card during the drag — competing pop-ups around the
    // cursor make it hard to see drop targets.
    window.mcalSetHoverDisabled && window.mcalSetHoverDisabled(true);
  };
  const onDragEnd = () => {
    setDraggingId(null);
    setDragHoverDate(null);
    window.mcalSetHoverDisabled && window.mcalSetHoverDisabled(false);
  };
  const onCellDragOver = (e, dateIso) => {
    // Allow drops from BOTH a top-level event drag (draggingId set in our
    // state) AND a sub-task drag (dataTransfer has 'text/plain' but no
    // local state). We can't read getData() in dragOver — Firefox limits
    // that to drop — so just check the types list.
    if (!draggingId && !(e.dataTransfer.types && e.dataTransfer.types.indexOf('text/plain') >= 0)) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
    setDragHoverDate(dateIso);
  };
  const onCellDrop = (e, dateIso) => {
    e.preventDefault();
    const raw = e.dataTransfer.getData('text/plain') || (draggingId || '');
    if (!raw) return;
    // A sub-task being dragged out → lift it to a standalone event on
    // the dropped date.
    if (raw.startsWith('subtask:') && liftSubtask) {
      const [, parentId, subId] = raw.split(':');
      liftSubtask(parentId, subId, dateIso);
      setDraggingId(null);
      setDragHoverDate(null);
      return;
    }
    const id = raw;
    const ev = events.find(x => x.id === id);
    if (!ev) return;
    const delta = UM.diffDays(ev.start, dateIso);
    // Shift sub-task dates by the same delta so they keep their offset
    // relative to the parent.
    const shiftedSubs = (ev.subtasks || []).map(st => ({
      ...st,
      date: UM.addDays(st.date, delta),
    }));
    update(id, {
      start: UM.addDays(ev.start, delta),
      end: UM.addDays(ev.end, delta),
      subtasks: shiftedSubs,
    });
    setDraggingId(null);
    setDragHoverDate(null);
  };

  // Drop on top of an existing pill/span-bar.
  // - Sub-task dragged onto its OWN parent's bar → reschedule the
  //   sub-task to the column under the cursor (stays a sub-task).
  // - Sub-task dragged onto a DIFFERENT event's bar → ignored (user can
  //   drop on a cell to lift, then drag onto the new parent if needed).
  // - Regular event dragged onto another event → nest as sub-task on
  //   the column under the cursor.
  const onPillDrop = (e, targetEv, dropDate) => {
    e.preventDefault();
    e.stopPropagation();
    const raw = e.dataTransfer.getData('text/plain') || (draggingId || '');
    const clear = () => { setDraggingId(null); setDragHoverDate(null); };
    if (!raw) { clear(); return; }

    if (raw.startsWith('subtask:')) {
      const [, parentId, subId] = raw.split(':');
      if (parentId === targetEv.id) {
        // Reschedule the sub-task to the dropped day.
        const newSubs = (targetEv.subtasks || []).map(s =>
          s.id === subId ? { ...s, date: dropDate } : s);
        update(targetEv.id, { subtasks: newSubs });
      } else if (moveSubtask) {
        // Move sub-task between parents.
        moveSubtask(parentId, subId, targetEv.id, dropDate);
      }
      clear();
      return;
    }

    if (raw === targetEv.id) { clear(); return; }
    if (nestAsSubtask) nestAsSubtask(raw, targetEv.id, dropDate);
    clear();
  };

  return (
    <div className={'month' + (fortnight ? ' is-fortnight' : '')}>
      <div className="month-dows">
        {UM.DOW_NAMES_SHORT.map(d => <div key={d} className="dow">{d}</div>)}
      </div>
      {weekRows.map((w, wi) => (
        <MonthWeekRow
          key={w.start}
          week={w}
          events={events}
          options={options}
          colorBy={colorBy}
          today={today}
          currentMonth={currentMonth}
          fortnight={fortnight}
          dragHoverDate={dragHoverDate}
          draggingId={draggingId}
          onDragStart={onDragStart}
          onDragEnd={onDragEnd}
          onCellDragOver={onCellDragOver}
          onCellDrop={onCellDrop}
          onPillDrop={onPillDrop}
          openEvent={openEvent}
          onAdd={(d) => addEventAt({ start: d, end: d })}
        />
      ))}
    </div>
  );
}

function MonthWeekRow({ week, events, options, colorBy, today, currentMonth, fortnight, dragHoverDate, draggingId, onDragStart, onDragEnd, onCellDragOver, onCellDrop, onPillDrop, openEvent, onAdd }) {
  // Events that overlap this week → assigned to lanes
  const overlaps = events.filter(ev => UM.eventOverlaps(ev, week.start, week.end));
  overlaps.sort((a, b) => {
    if (a.start !== b.start) return a.start < b.start ? -1 : 1;
    const da = UM.diffDays(a.start, a.end);
    const db = UM.diffDays(b.start, b.end);
    if (da !== db) return db - da;
    return a.title < b.title ? -1 : 1;
  });
  const lanes = [];
  const placed = overlaps.map(ev => {
    const startCol = Math.max(0, UM.diffDays(week.start, ev.start));
    const endCol = Math.min(6, UM.diffDays(week.start, ev.end));
    let laneIdx = lanes.findIndex(e => e < startCol);
    if (laneIdx === -1) { laneIdx = lanes.length; lanes.push(endCol); }
    else lanes[laneIdx] = endCol;
    return { ev, startCol, endCol, lane: laneIdx };
  });

  // Fortnight rows have unconstrained vertical room (block layout with
  // overflow: hidden on each row), so there's no reason to ever truncate
  // events into "+N more" — the row simply grows to fit. The month grid
  // still caps at 3 lanes because each of its 6 rows is short.
  const MAX_VISIBLE_LANES = fortnight ? 999 : 3;

  // For each parent: subtasks visible this week, plus the per-column stack
  // depth so we can vertically stack multiple sub-tasks that share a day.
  const subBandByParent = new Map();
  placed.forEach(p => {
    const list = (p.ev.subtasks || []).filter(st => st.date >= week.start && st.date <= week.end);
    if (!list.length) return;
    const perCol = {};
    list.forEach(st => {
      const col = UM.diffDays(week.start, st.date);
      if (col < 0 || col > 6) return;
      if (!perCol[col]) perCol[col] = [];
      perCol[col].push(st);
    });
    const maxStack = Object.values(perCol).reduce((m, arr) => Math.max(m, arr.length), 0);
    subBandByParent.set(p.ev.id, { list, perCol, maxStack });
  });

  const barH = 22;
  const subH = 20;
  const subGap = 2;
  const gapAfterLane = 4;
  const headerH = 24;

  // Lane-level top calculation.
  // Every event at lane K shares the same y, regardless of which
  // columns it occupies. This is more conservative on vertical space
  // (a higher lane is always pushed below ALL lower lanes' sub-bands,
  // even when those sub-bands aren't in this event's columns) but it
  // is the only model that guarantees zero visual overlap between
  // events at different lanes — the previous column-aware version
  // could produce different y values for two events at lane K, which
  // visually looked like overlap even when the columns were disjoint.
  const maxStackByLane = new Map();
  let maxLane = -1;
  placed.forEach(p => {
    if (p.lane > maxLane) maxLane = p.lane;
    const band = subBandByParent.get(p.ev.id);
    const stack = band ? band.maxStack : 0;
    const cur = maxStackByLane.get(p.lane) || 0;
    if (stack > cur) maxStackByLane.set(p.lane, stack);
  });
  const laneTops = [headerH];
  for (let k = 0; k <= maxLane; k++) {
    const stack = maxStackByLane.get(k) || 0;
    laneTops[k + 1] = laneTops[k] + barH + stack * (subH + subGap) + gapAfterLane;
  }
  const tops = new Map();
  placed.forEach(p => tops.set(p.ev.id, laneTops[p.lane]));

  // Row height: max bottom of any placed bar + its full sub-stack height
  let contentBottom = headerH;
  placed.forEach(p => {
    if (p.lane >= MAX_VISIBLE_LANES) return;
    const top = tops.get(p.ev.id);
    let bot = top + barH;
    const band = subBandByParent.get(p.ev.id);
    if (band && band.maxStack > 0) bot += band.maxStack * (subH + subGap);
    if (bot > contentBottom) contentBottom = bot;
  });

  const overflowByDay = {};
  for (let c = 0; c <= 6; c++) overflowByDay[c] = 0;
  placed.forEach(p => {
    if (p.lane >= MAX_VISIBLE_LANES) {
      for (let c = p.startCol; c <= p.endCol; c++) overflowByDay[c]++;
    }
  });
  // Each row claims at least this many pixels even when sparse, so the
  // 2-week grid still feels filled. If `contentBottom + 14` exceeds the
  // floor, the row grows past it (block layout, no grid clipping).
  // The CSS sets `overflow: hidden` on fortnight rows as a backstop in
  // case contentBottom under-counts by a pixel or two.
  const minRow = fortnight ? 420 : 110;
  const rowStyle = { minHeight: Math.max(minRow, contentBottom + 14) };

  return (
    <div className="month-row" style={rowStyle}>
      {[0,1,2,3,4,5,6].map(c => {
        const dateIso = UM.addDays(week.start, c);
        // Fortnight grid always shows exactly 2 weeks centered on the
        // cursor — there's no "off-month" concept since every cell is
        // part of the visible window. Instead, dim Sat + Sun so the
        // weekdays still pop.
        const isOff = fortnight
          ? UM.isWeekend(dateIso)
          : UM.parseISO(dateIso).getUTCMonth() !== currentMonth;
        const isToday = dateIso === today;
        const dayN = UM.dayOfMonth(dateIso);
        const cls = ['month-cell'];
        if (isOff) cls.push('off');
        if (isToday) cls.push('today');
        if (dragHoverDate === dateIso) cls.push('drop-target');
        return (
          <div
            key={c}
            className={cls.join(' ')}
            onDragOver={e => onCellDragOver(e, dateIso)}
            onDrop={e => onCellDrop(e, dateIso)}
          >
            <div className="daynum-wrap">
              <span className={'daynum' + (dayN === 1 ? ' first' : '')}>
                {dayN === 1 && <span className="month-prefix">{UM.monthShort(dateIso)}</span>}
                {dayN}
              </span>
              <button className="quick-add" onClick={e => { e.stopPropagation(); onAdd(dateIso); }} title="Add event">+</button>
            </div>
            {overflowByDay[c] > 0 && (
              <div className="more-link" style={{ position: 'absolute', bottom: 4, left: 6 }}>
                +{overflowByDay[c]} more
              </div>
            )}
          </div>
        );
      })}

      {/* Sub-pills — absolutely positioned directly under their parent's
          span bar. Multiple sub-tasks on the same day stack vertically. */}
      {placed.filter(p => p.lane < MAX_VISIBLE_LANES).flatMap(p => {
        const band = subBandByParent.get(p.ev.id);
        if (!band) return [];
        const colW = 100 / 7;
        const baseTop = tops.get(p.ev.id) + barH + subGap;
        const out = [];
        Object.entries(band.perCol).forEach(([colStr, list]) => {
          const col = +colStr;
          if (col < p.startCol || col > p.endCol) return;
          list.forEach((st, idx) => {
            out.push(
              <SubtaskAbsPill
                key={p.ev.id + '_' + st.id}
                st={st}
                ev={p.ev}
                options={options}
                style={{
                  left: `calc(${col * colW}% + 2px)`,
                  width: `calc(${colW}% - 4px)`,
                  top: baseTop + idx * (subH + subGap),
                }}
                onClick={() => openEvent(p.ev.id, st.id)}
              />
            );
          });
        });
        return out;
      })}

      {placed.filter(p => p.lane < MAX_VISIBLE_LANES).map(p => {
        const colW = 100 / 7;
        const left = p.startCol * colW;
        const width = (p.endCol - p.startCol + 1) * colW;
        const top = tops.get(p.ev.id);
        const color = colorForEvent(p.ev, options, colorBy);
        const isPastDue = isPastDueEvent(p.ev, today, options);
        const cls = ['span-bar'];
        if (p.ev.tbd && p.ev.stage !== 'done') cls.push('tbd');
        if (p.ev.needsReview && p.ev.stage !== 'done') cls.push('needs-review');
        if (isPastDue && !p.ev.isHoliday) cls.push('past-due');
        if (draggingId === p.ev.id) cls.push('dragging');
        if (p.ev.isHoliday) cls.push('holiday');
        const isSingleCellShort = (p.endCol - p.startCol) === 0;
        return (
          <window.mcalHoverCard.HoverWrap
            key={p.ev.id}
            event={p.ev}
            options={options}
            onEdit={() => openEvent(p.ev.id)}
          >
            {triggerProps => (
              <div
                className={cls.join(' ')}
                draggable={!p.ev.isHoliday && !p.ev.isConference}
                onDragStart={e => onDragStart(e, p.ev)}
                onDragEnd={onDragEnd}
                onDragOver={e => {
                  if (draggingId === p.ev.id) return; // not on self
                  // If the dragged event already has sub-tasks, nesting it
                  // here would silently destroy its sub-task list. Let the
                  // event bubble to the cell so the user can re-date instead.
                  if (draggingId) {
                    const dragged = events.find(x => x.id === draggingId);
                    if (dragged && dragged.subtasks && dragged.subtasks.length > 0) return;
                  }
                  e.preventDefault();
                  e.stopPropagation();
                  e.dataTransfer.dropEffect = 'move';
                  if (e.currentTarget && !e.currentTarget.classList.contains('drop-nest-target')) {
                    e.currentTarget.classList.add('drop-nest-target');
                  }
                }}
                onDragLeave={e => { e.currentTarget.classList.remove('drop-nest-target'); }}
                onDrop={e => {
                  e.currentTarget.classList.remove('drop-nest-target');
                  // Figure out which column was dropped on for multi-day bars
                  const rect = e.currentTarget.getBoundingClientRect();
                  const fraction = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
                  const numCols = p.endCol - p.startCol + 1;
                  const colOffset = Math.min(numCols - 1, Math.floor(fraction * numCols));
                  const dropDate = UM.addDays(p.ev.start, colOffset);
                  onPillDrop(e, p.ev, dropDate);
                }}
                onClick={e => { e.stopPropagation(); openEvent(p.ev.id); }}
                {...triggerProps}
                style={{
                  left: `calc(${left}% + 2px)`,
                  width: `calc(${width}% - 4px)`,
                  top: top,
                  borderLeftColor: color,
                }}
              >
                <window.mcalStageGlyphPill stageId={p.ev.stage} options={options} />
                <span className="ptitle">{window.mcalRichText ? window.mcalRichText(p.ev.title, options) : p.ev.title}</span>
                {!isSingleCellShort && <window.mcalProductTag event={p.ev} options={options} />}
                {(p.ev.subtasks || []).length > 0 && (
                  <span style={{
                    fontFamily: 'var(--font-mono)', fontSize: 9, color: 'var(--fg-3)',
                    letterSpacing: '0.04em', padding: '0 4px',
                  }} title={`${p.ev.subtasks.length} sub-tasks`}>
                    +{p.ev.subtasks.length}
                  </span>
                )}
              </div>
            )}
          </window.mcalHoverCard.HoverWrap>
        );
      })}
    </div>
  );
}

/* Sub-task hover wrapper. Passes both the parent event and the sub-task to the
   hover card so it can render sub-task specific info while still letting Edit
   open the parent's slide-over (sub-tasks live inside the parent). */
function SubtaskHover({ st, parent, options, onEdit, children }) {
  return (
    <window.mcalHoverCard.HoverWrap event={parent} subtask={st} options={options} onEdit={onEdit}>
      {children}
    </window.mcalHoverCard.HoverWrap>
  );
}

function SubtaskAbsPill({ st, ev, options, style, onClick }) {
  // Effective category: sub-task override → parent fallback
  const effCat = (st.category !== undefined && st.category !== null && st.category !== '') ? st.category : ev.category;
  const cat = UM.colorOf(options, 'categories', effCat);
  const stage = UM.stageById(options, st.stage || 'ideation');
  const StageGlyph = window.mcalStageGlyph;
  return (
    <SubtaskHover st={st} parent={ev} options={options} onEdit={onClick}>
      {tp => (
        <div
          className={'subpill abs'
            + (st.stage === 'done' ? ' done' : '')
            + (st.needsReview && st.stage !== 'done' ? ' needs-review' : '')}
          style={{
            position: 'absolute',
            borderLeftColor: cat,
            ...(style || {}),
          }}
          draggable
          onDragStart={e => {
            e.stopPropagation();
            e.dataTransfer.effectAllowed = 'move';
            e.dataTransfer.setData('text/plain', 'subtask:' + ev.id + ':' + st.id);
            window.mcalSetHoverDisabled && window.mcalSetHoverDisabled(true);
          }}
          onDragEnd={() => { window.mcalSetHoverDisabled && window.mcalSetHoverDisabled(false); }}
          onClick={e => { e.stopPropagation(); onClick && onClick(); }}
          {...tp}
        >
          <span style={{ color: stage.color || 'var(--fg-3)', flexShrink: 0, display: 'inline-flex' }}>
            {StageGlyph ? <StageGlyph pct={stage.fillPct != null ? stage.fillPct : ((stage.order + 1) / 4) * 100} size={9} /> : stage.glyph}
          </span>
          <span className="stitle">{window.mcalRichText ? window.mcalRichText(st.title, options) : st.title}</span>
        </div>
      )}
    </SubtaskHover>
  );
}

function SubtaskPill({ st, ev, options, onClick }) {
  // color the left edge by the sub-task's effective category (override → parent)
  const effCat = (st.category !== undefined && st.category !== null && st.category !== '') ? st.category : ev.category;
  const cat = UM.colorOf(options, 'categories', effCat);
  const stage = UM.stageById(options, st.stage || 'ideation');
  return (
    <div
      className={'subpill' + (st.stage === 'done' ? ' done' : '')}
      style={{ borderLeftColor: cat }}
      onClick={e => { e.stopPropagation(); onClick(); }}
      title={`${ev.title}\n  ↳ ${st.title}\nStage: ${stage.name}`}
    >
      <span style={{ fontSize: 10, color: stage.color || 'var(--fg-3)' }}>{stage.glyph}</span>
      <span className="stitle">{st.title}</span>
    </div>
  );
}

/* "Color by" can be set to owners — but an event can now have multiple
   owners. Use the first owner for the color, since that maps to a single
   border color cleanly. */
function colorForEvent(ev, options, colorBy) {
  if (colorBy === 'owners') {
    const ids = UM.ownerIdsOf(ev);
    return UM.colorOf(options, 'owners', ids[0] || '');
  }
  return UM.colorOf(options, colorBy, ev[singularFor(colorBy)]);
}

function singularFor(plural) {
  return plural === 'channels' ? 'channel'
    : plural === 'categories' ? 'category'
    : plural === 'owners' ? 'owner'
    : plural === 'products' ? 'product'
    : plural === 'stages' ? 'stage'
    : 'category';
}

function isPastDueEvent(ev, today, options) {
  if (ev.tbd) return false;
  if (ev.end >= today) return false;
  const order = UM.stageOrder(options, ev.stage);
  return order < 2;
}

/* React.memo the view so it doesn't re-render when DetailPanel patches
   land on App state but events/cursor/etc. are unchanged. Default shallow
   prop comparison is enough since App passes the same callback refs. */
const MonthViewMemo = React.memo(MonthView);
window.mcalMonth = { MonthView: MonthViewMemo };
