/* Dump — scheduler.jsx: AI scheduling suggestions.
   The user has floating tasks (dueDate set, no time). This looks at the day's
   anchors + events, finds the real free gaps, and proposes times. The user
   confirms ("Looks good") or tweaks each block (retime / remove). Nothing
   moves without confirmation. Opens as a docked sheet over the Calendar.

   Honest free-time math lives here (UNION of busy intervals, not a sum), and
   it never schedules into the past for today. Demo mode uses a deterministic
   mockSchedule that obeys the same rules so the AI-off demo still looks good.

   Reuses window helpers from calendar.jsx: calParse, calRange, anchorsForDay,
   hmToMin, minToHm, GridBlock, CAL_MONTHS, CAL_DOW. */

/* ---------- free-time math ---------- */
function schedMergeIntervals(intervals) {
  const a = intervals.filter((x) => x[1] > x[0]).sort((p, q) => p[0] - q[0]);
  const out = [];
  a.forEach((iv) => {
    const last = out[out.length - 1];
    if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1]);
    else out.push(iv.slice());
  });
  return out;
}
function schedFreeGaps(winS, winE, busy) {
  const gaps = [];
  let cur = winS;
  busy.forEach((iv) => {
    const cs = Math.max(iv[0], winS), ce = Math.min(iv[1], winE);
    if (ce <= winS || cs >= winE) return;
    if (cs > cur) gaps.push([cur, cs]);
    cur = Math.max(cur, ce);
  });
  if (cur < winE) gaps.push([cur, winE]);
  return gaps.filter((g) => g[1] - g[0] >= 5);
}

/* ---------- context the AI (or mock) works from ---------- */
function buildSchedulingContext(date) {
  const s = DumpStore.getState();
  const dObj = window.calParse(date);
  const dx = s.settings.dx || {};
  const winRange = window.calRange(dx);
  let winS = winRange[0];
  const winE = winRange[1];

  // For today, free time starts at "now" (rounded up to 15) — never the past.
  const todayIso = DumpUtil.todayISO();
  if (date === todayIso) {
    const now = new Date();
    const nowMin = Math.ceil((now.getHours() * 60 + now.getMinutes()) / 15) * 15;
    winS = Math.max(winS, nowMin);
  }

  // Anchors active on this day.
  const anchorsFull = window.anchorsForDay(s.anchors, dObj).map((a) => ({
    name: a.name, _s: a.start, _e: a.start + (a.duration || 30),
    start: window.minToHm(a.start), end: window.minToHm(a.start + (a.duration || 30))
  }));

  // Events (same source the calendar renders). Durations unknown → assume 60.
  let evRaw = [];
  if (s.google.connected) {
    if (s.google.demo && window.DumpDemo && DumpDemo.rangeEvents) {
      evRaw = DumpDemo.rangeEvents().filter((e) => e.date === date);
    } else if (date === todayIso) {
      evRaw = (s.google.events || []).map((e) => ({ time: e.time, title: e.title }));
    }
  }
  const eventsFull = evRaw
    .map((e) => ({ title: e.title, _s: window.hmToMin(e.time), _e: window.hmToMin(e.time) + 60 }))
    .filter((e) => e._s != null)
    .map((e) => ({ title: e.title, _s: e._s, _e: e._e, start: window.minToHm(e._s), end: window.minToHm(e._e) }));

  // Floating tasks for this date: dated, no time, not a project, active.
  const tasks = s.tasks
    .filter((t) => !t.done && t.dueDate === date && (!t.status || t.status === 'active') && !t.time && t.big !== true)
    .map((t) => ({
      id: t.id, text: t.text, minutes: (t.minutes || 30),
      priority: (t.level === 'high' || t.priority) ? 'high' : 'normal', category: t.category
    }));

  const busy = schedMergeIntervals(
    anchorsFull.map((a) => [a._s, a._e]).concat(eventsFull.map((e) => [e._s, e._e]))
  );
  const gaps = schedFreeGaps(winS, winE, busy);
  const freeMinutes = gaps.reduce((n, g) => n + (g[1] - g[0]), 0);

  const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  return {
    date: date,
    dayName: dayNames[dObj.getDay()],
    userName: (s.settings.userName || '').trim() || null,
    anchors: anchorsFull.map((a) => ({ name: a.name, start: a.start, end: a.end })),
    events: eventsFull.map((e) => ({ title: e.title, start: e.start, end: e.end })),
    tasks: tasks,
    freeMinutes: freeMinutes,
    // private helpers for the renderer + mock (not sent to the AI)
    _winS: winS, _winE: winE, _gaps: gaps, _anchorsFull: anchorsFull, _eventsFull: eventsFull
  };
}

/* ---------- prompt context (serialisable; text lives in prompts-core.js) ----------
   Pre-formats the free-time math into plain strings so the prompt builder stays
   pure (no window.minToHm). This object is what gets POSTed to the worker. */
function schedulePromptCtx(ctx) {
  return {
    userName: ctx.userName,
    dayName: ctx.dayName,
    date: ctx.date,
    freeMinutes: ctx.freeMinutes,
    winStart: window.minToHm(ctx._winS),
    winEnd: window.minToHm(ctx._winE),
    anchors: ctx.anchors,
    events: ctx.events,
    gaps: (ctx._gaps || []).map((g) => ({ start: window.minToHm(g[0]), end: window.minToHm(g[1]), min: g[1] - g[0] })),
    tasks: ctx.tasks
  };
}

/* ---------- validation: keep suggestions honest ----------
   The prompt ASKS for no-overlap, a 15-min gap and a 70% cap; this GUARANTEES
   all three on the model's output (same "prompt asks, code verifies" pattern as
   the rest of the app) — dropping any slot that violates them. */
function schedSanitise(suggestions, ctx) {
  if (!Array.isArray(suggestions)) return [];
  const taskById = {};
  ctx.tasks.forEach((t) => { taskById[t.id] = t; });
  const fitsGap = (s, e) => (ctx._gaps || []).some((g) => s >= g[0] && e <= g[1]);
  const TRANS = 15;
  const cap = ctx.freeMinutes ? Math.floor(ctx.freeMinutes * 0.7) : Infinity;
  const used = {};
  const placed = []; // {start,end} already accepted, to enforce the 15-min gap
  let booked = 0;
  return suggestions
    .filter((sg) => sg && sg.taskId && /^\d{2}:\d{2}$/.test(sg.time || ''))
    .map((sg) => ({ sg: sg, start: window.hmToMin(sg.time) }))
    .sort((a, b) => a.start - b.start) // evaluate in clock order so gaps compute correctly
    .filter(({ sg, start }) => {
      const t = taskById[sg.taskId];
      if (!t || used[sg.taskId]) return false;
      const dur = t.minutes || 30;
      const end = start + dur;
      if (!fitsGap(start, end)) return false;                         // no overlap / within window
      if (booked + dur > cap && placed.length) return false;          // 70% cap (always allow the first)
      if (placed.some((p) => start < p.end + TRANS && end + TRANS > p.start)) return false; // 15-min gap
      used[sg.taskId] = true;
      placed.push({ start, end });
      booked += dur;
      return true;
    })
    .map(({ sg }) => ({ taskId: sg.taskId, time: sg.time, reason: (sg.reason || '').toString().slice(0, 90) }))
    .sort((a, b) => window.hmToMin(a.time) - window.hmToMin(b.time));
}

/* ---------- demo-mode scheduler: deterministic, obeys the same rules ---------- */
function mockReason(startMin, dur, task, ctx) {
  const af = (ctx._anchorsFull || []).filter((a) => a._e <= startMin).sort((a, b) => b._e - a._e)[0];
  const beforeEv = (ctx._eventsFull || []).filter((e) => e._s >= startMin + dur).sort((a, b) => a._s - b._s)[0];
  if (af && startMin - af._e <= 35) return 'Right after ' + af.name.toLowerCase();
  if (beforeEv && beforeEv._s - (startMin + dur) <= 60) return 'A quiet slot before ' + beforeEv.title.toLowerCase();
  if (dur <= 10) return 'A quick one to slot in';
  if (task.priority === 'high') return 'Early, while you’ve got the energy';
  if (task.category === 'Work') return 'In your work stretch';
  return 'A clear stretch in your day';
}
function mockSchedule(ctx) {
  const gaps = (ctx._gaps || []).map((g) => g.slice());
  const cursors = gaps.map((g) => g[0]);
  const cap = Math.max(ctx.freeMinutes ? Math.floor(ctx.freeMinutes * 0.7) : 0, 0);
  const TRANS = 15;
  // HIGH first, then keep input order (stable).
  const tasks = ctx.tasks
    .map((t, i) => ({ t: t, i: i }))
    .sort((a, b) => ((b.t.priority === 'high' ? 1 : 0) - (a.t.priority === 'high' ? 1 : 0)) || (a.i - b.i))
    .map((x) => x.t);

  let used = 0;
  const out = [];
  tasks.forEach((task) => {
    const dur = task.minutes || 30;
    if (out.length && used + dur > cap) return; // 70% cap (always allow the first)
    for (let i = 0; i < gaps.length; i++) {
      const startAt = cursors[i];
      if (startAt + dur <= gaps[i][1]) {
        out.push({ taskId: task.id, time: window.minToHm(startAt), reason: mockReason(startAt, dur, task, ctx) });
        cursors[i] = startAt + dur + TRANS;
        used += dur;
        return;
      }
    }
  });
  return out.sort((a, b) => window.hmToMin(a.time) - window.hmToMin(b.time));
}

/* ---------- the API call ---------- */
async function suggestSchedule(date) {
  const ctx = buildSchedulingContext(date);
  if (!ctx.tasks.length) return { suggestions: [], ctx: ctx };
  if (!DumpAPI.aiOn()) {
    await new Promise((r) => setTimeout(r, 700)); // a calm, considered beat
    return { suggestions: mockSchedule(ctx), ctx: ctx };
  }
  try {
    const reply = await DumpAPI.runAI(
      'schedule',
      schedulePromptCtx(ctx),
      'Plan my ' + ctx.dayName + '. Return the JSON array now.',
      700
    );
    const sane = schedSanitise(DumpAPI.parseJSONReply(reply), ctx);
    // If the sanitiser dropped everything (bad slots from the model), fall back
    // to the deterministic plan rather than falsely reporting "day too full" —
    // the mock returns [] only when the day genuinely has no room.
    return { suggestions: sane.length ? sane : mockSchedule(ctx), ctx: ctx };
  } catch (e) {
    // Never dead-end the flow — fall back to the deterministic plan.
    return { suggestions: mockSchedule(ctx), ctx: ctx, aiError: e && e.friendly ? e.message : null };
  }
}

Object.assign(window, { buildSchedulingContext: buildSchedulingContext, suggestSchedule: suggestSchedule });
DumpAPI.suggestSchedule = suggestSchedule;

/* ---------- now-aware free window (for the Focus session reality check) ----------
   Not today's TOTAL free time — the single next window starting from now: the
   gap from this moment until the next anchor or event. This is what makes the
   session feel grounded ("1 hr 20 free now") rather than abstract. */
function nextFreeWindow(date) {
  let ctx;
  try { ctx = buildSchedulingContext(date); } catch (e) { return { start: null, end: null, minutes: 0, startsNow: false }; }
  const gaps = ctx._gaps || [];
  if (!gaps.length) return { start: null, end: null, minutes: 0, startsNow: false };
  const first = gaps[0];
  let startsNow = false;
  if (date === DumpUtil.todayISO()) {
    const n = new Date();
    const nowMin = n.getHours() * 60 + n.getMinutes();
    startsNow = first[0] <= nowMin + 2; // the window begins (essentially) now
  }
  return { start: first[0], end: first[1], minutes: first[1] - first[0], startsNow: startsNow };
}
window.nextFreeWindow = nextFreeWindow;
DumpAPI.nextFreeWindow = nextFreeWindow;

/* ============================================================
   The sheet
   ============================================================ */
const SCHED_HOUR_PX = 50;

function SchedBlock({ item, top, height }) {
  return window.GridBlock({ item: item, top: top, height: height });
}

function Scheduler({ date, onClose }) {
  const { settings } = useDumpStore();
  const [phase, setPhase] = useState('loading'); // loading | plan | empty
  const [ctx, setCtx] = useState(null);
  const [sugs, setSugs] = useState([]);
  const [editId, setEditId] = useState(null);
  const [fromSaved, setFromSaved] = useState(false);

  // A fresh AI/mock proposal. Also used by "Re-plan from scratch" when reopening
  // a day that already had a saved plan.
  const proposeFresh = () => {
    setEditId(null);
    setFromSaved(false);
    setPhase('loading');
    DumpAPI.suggestSchedule(date).then((res) => {
      setCtx(res.ctx);
      setSugs(res.suggestions);
      setPhase(res.suggestions.length ? 'plan' : 'empty');
    });
  };

  useEffect(() => {
    let alive = true;
    setPhase('loading');
    setEditId(null);
    // Reopening a day that already has a saved plan should LOAD it, not re-propose
    // from scratch. Keep only entries whose task is still floating on this day.
    const saved = (DumpStore.getState().dayPlan || {})[date];
    const ctxNow = window.buildSchedulingContext(date);
    const validIds = {};
    ctxNow.tasks.forEach((t) => { validIds[t.id] = true; });
    const savedEntries = saved && Array.isArray(saved.entries)
      ? saved.entries.filter((e) => e && e.taskId && e.time && validIds[e.taskId])
      : [];
    if (savedEntries.length) {
      setCtx(ctxNow);
      setSugs(savedEntries.map((e) => ({ taskId: e.taskId, time: e.time, reason: e.reason || '' })));
      setFromSaved(true);
      setPhase('plan');
      return () => { alive = false; };
    }
    setFromSaved(false);
    DumpAPI.suggestSchedule(date).then((res) => {
      if (!alive) return;
      setCtx(res.ctx);
      setSugs(res.suggestions);
      setPhase(res.suggestions.length ? 'plan' : 'empty');
    });
    return () => { alive = false; };
  }, [date]);

  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);

  const d = window.calParse(date);
  const isToday = date === DumpUtil.todayISO();
  const dayWord = isToday ? 'today' : window.CAL_DOW[(d.getDay() + 6) % 7];
  const titleLabel = (isToday ? 'Today · ' : '') + window.CAL_DOW[(d.getDay() + 6) % 7] + ' ' + d.getDate() + ' ' + window.CAL_MONTHS[d.getMonth()];

  const winS = ctx ? ctx._winS : 480;
  const winE = ctx ? ctx._winE : 1140;
  const pxMin = SCHED_HOUR_PX / 60;
  const bodyH = (winE - winS) * pxMin;
  const hours = [];
  for (let m = Math.ceil(winS / 60) * 60; m <= winE; m += 60) hours.push(m);

  const taskById = {};
  if (ctx) ctx.tasks.forEach((t) => { taskById[t.id] = t; });

  const retime = (taskId, time) => setSugs((arr) => arr.map((s) => (s.taskId === taskId ? Object.assign({}, s, { time: time }) : s)));
  const removeOne = (taskId) => { setSugs((arr) => arr.filter((s) => s.taskId !== taskId)); setEditId(null); };

  const confirm = () => {
    const valid = sugs.filter((s) => s.time);
    DumpAPI.saveDayPlan(date, valid);
    DumpAPI.toast('Plan saved for ' + dayWord + ' — times are suggestions; your calendar stays untouched.', {
      action: { label: 'View on calendar', fn: () => DumpStore.set({ route: 'calendar' }) }
    });
    onClose();
  };

  const moveToTomorrow = () => {
    const t = new Date(d); t.setDate(t.getDate() + 1); t.setHours(12, 0, 0, 0);
    const iso = t.getFullYear() + '-' + String(t.getMonth() + 1).padStart(2, '0') + '-' + String(t.getDate()).padStart(2, '0');
    DumpStore.set({ scheduler: { open: true, date: iso } });
  };

  return (
    <React.Fragment>
      <div className="sched-scrim" onClick={onClose}></div>
      <div className="sched-sheet" role="dialog" aria-label="Plan your day">
        <div className="sched-head">
          <div>
            <p className="sched-kicker">{Icons.sparkle ? Icons.sparkle(13) : null} Suggested plan</p>
            <h2 className="sched-title">{titleLabel}</h2>
          </div>
          <button className="icon-btn" onClick={onClose} aria-label="Close">{Icons.x(17)}</button>
        </div>

        {phase === 'loading' ? (
          <div className="sched-loading">
            <span className="spinner dark"></span>
            <p>Finding gaps in your day…</p>
          </div>
        ) : phase === 'empty' ? (
          <div className="sched-empty">
            {ctx && ctx.tasks.length ? (
              <React.Fragment>
                <p className="sched-empty-title">Your {dayWord} is too full to fit these in.</p>
                <p className="sched-empty-sub">Try moving an event, or push a task to tomorrow — there’s usually more room.</p>
                <div className="sched-foot">
                  <button className="btn btn-primary btn-sm" onClick={moveToTomorrow}>Plan tomorrow instead →</button>
                  <button className="btn btn-ghost btn-sm" onClick={onClose}>Not now</button>
                </div>
              </React.Fragment>
            ) : (
              <React.Fragment>
                <p className="sched-empty-title">Nothing to slot in {isToday ? 'today' : 'that day'}.</p>
                <p className="sched-empty-sub">Give a task a date with no time and it’ll show up here, ready to schedule.</p>
                <div className="sched-foot">
                  <button className="btn btn-ghost btn-sm" onClick={onClose}>Close</button>
                </div>
              </React.Fragment>
            )}
          </div>
        ) : (
          <React.Fragment>
            <p className="sched-sub">
              {fromSaved
                ? <React.Fragment>Here’s the plan you saved for {dayWord} — tap any block to change it, or <button className="linklike" onClick={proposeFresh}>re-plan from scratch</button>.</React.Fragment>
                : <React.Fragment>{sugs.length} task{sugs.length === 1 ? '' : 's'} slotted into your free time — tap any block to change its time or take it off.</React.Fragment>}
            </p>
            <div className="sched-body" style={{ height: bodyH }}>
              <div className="sched-axis">
                {hours.map((m) => (
                  <span key={m} className="sched-axis-h" style={{ top: (m - winS) * pxMin }}>{window.minToHm(m)}</span>
                ))}
              </div>
              <div className="sched-col"
                style={{ backgroundImage: 'repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px ' + SCHED_HOUR_PX + 'px)' }}>
                {ctx._anchorsFull.map((a, i) => {
                  const top = Math.max(0, (a._s - winS) * pxMin);
                  const h = Math.max((Math.min(a._e, winE) - Math.max(a._s, winS)) * pxMin, 16);
                  if (a._e <= winS) return null;
                  return <SchedBlock key={'a' + i} item={{ kind: 'anchor', name: a.name }} top={top} height={h} />;
                })}
                {ctx._eventsFull.map((e, i) => {
                  if (e._e <= winS) return null;
                  const top = Math.max(0, (e._s - winS) * pxMin);
                  const h = Math.max((Math.min(e._e, winE) - Math.max(e._s, winS)) * pxMin, 18);
                  return <SchedBlock key={'e' + i} item={{ kind: 'event', title: e.title, time: e.start }} top={top} height={h} />;
                })}
                {sugs.map((sg) => {
                  const t = taskById[sg.taskId];
                  if (!t) return null;
                  const start = window.hmToMin(sg.time);
                  const top = Math.max(0, (start - winS) * pxMin);
                  const h = Math.max((t.minutes || 30) * pxMin, 30);
                  const open = editId === sg.taskId;
                  return (
                    <div key={sg.taskId} className={'tgrid-block sched-prop' + (open ? ' editing' : '')}
                      style={{ top: top, height: h }}
                      onClick={(ev) => { ev.stopPropagation(); setEditId(open ? null : sg.taskId); }}>
                      <span className="tgb-title">{t.text}</span>
                      <span className="sched-prop-meta"><span className="tgb-time">{sg.time}</span>{sg.reason ? <em className="sched-reason"> · {sg.reason}</em> : null}</span>
                    </div>
                  );
                })}
              </div>
            </div>

            {editId ? (() => {
              const sg = sugs.find((x) => x.taskId === editId);
              const t = taskById[editId];
              if (!sg || !t) return null;
              return (
                <div className="sched-edit">
                  <div className="sched-edit-row">
                    <span className="sched-edit-name">{t.text}</span>
                    <button className="linklike" onClick={() => setEditId(null)}>done</button>
                  </div>
                  <div className="sched-edit-row">
                    <label>Time</label>
                    <input type="time" value={sg.time} onChange={(e) => retime(editId, e.target.value)} />
                    <span className="sched-edit-dur">~{t.minutes || 30} min</span>
                    <span className="qa-spacer"></span>
                    <button className="linklike danger" onClick={() => removeOne(editId)}>Remove time</button>
                  </div>
                  {(() => {
                    const s = window.hmToMin(sg.time);
                    const dur = t.minutes || 30;
                    const hit = (ctx._anchorsFull || []).find((a) => s < a._e && (s + dur) > a._s);
                    return hit ? <p className="sched-edit-warn">Heads up — this overlaps {hit.name}. Allowed, just flagged.</p> : null;
                  })()}
                </div>
              );
            })() : null}

            <div className="sched-foot">
              <button className="btn btn-sched-go" onClick={confirm} disabled={!sugs.length}>{fromSaved ? 'Save changes →' : 'Looks good →'}</button>
              <span className="sched-foot-note">{ctx ? Math.round(ctx.freeMinutes / 60 * 10) / 10 : 0} hrs free {isToday ? 'left ' : ''}· nothing moves until you tap</span>
            </div>
          </React.Fragment>
        )}
      </div>
    </React.Fragment>
  );
}

window.Scheduler = Scheduler;
