// Calendar — week grid of GHL day-pass bookings.
// Wired to GET /api/v2/calendar. Falls back to a simple list if the week is
// empty of bookings.

function CalendarView({ onNavigate }) {
  // 7 columns + a time gutter does not fit a 360-450px viewport without
  // truncating day headers and clipping event names. Default to day view
  // on phone-sized screens; the user can toggle to week explicitly.
  const [view, setView] = React.useState(
    typeof window !== 'undefined' && window.innerWidth <= 480 ? 'day' : 'week'
  );
  // weekStart is the Monday at 00:00 *gym-local* of the currently-displayed
  // week. We store it as a JS Date pinned to that instant; UI labels are then
  // formatted in GYM_TZ via toLocaleDateString({ timeZone: GYM_TZ }).
  const [weekStart, setWeekStart] = React.useState(() => mondayOfGymWeek(new Date()));
  const cal = useAsync(() => api.get('/api/v2/calendar?limit=500'), []);

  // Grid hours. HOUR_END=20 caps the visible day at 8 PM so the 7:30 PM
  // booking cutoff (BOOKING_CUTOFF in app/routers/webhooks.py) sits with
  // 30 min of visual margin below it. Real enforcement is in the GHL
  // widget; a horizontal cutoff line is drawn at 7:30 PM as a reminder.
  const HOUR_START = 5;
  const HOUR_END   = 20;
  const PX_PER_MIN = 1.4;
  // Booking cutoff in gym-local minutes from midnight. Mirrors
  // BOOKING_CUTOFF_HOUR/MINUTE in webhooks.py.
  const BOOKING_CUTOFF_MIN = 19 * 60 + 30;

  const weekDays = React.useMemo(() => {
    // Identify each day in *gym-local* time. iso comes from gymIsoDay so it
    // matches the iso used to group bookings; labels are formatted with
    // timeZone: GYM_TZ so they render the gym's calendar date, not the
    // browser's. "Today" is whatever gym-local day contains the current
    // instant — independent of where the staff member is sitting.
    const todayIso = gymIsoDay(new Date());
    return Array.from({ length: 7 }, (_, i) => {
      const d = new Date(weekStart.getTime() + i * 86400000);
      const iso = gymIsoDay(d);
      return {
        iso,
        d:    d.toLocaleDateString(undefined, { timeZone: GYM_TZ, weekday: 'short' }),
        date: d.toLocaleDateString(undefined, { timeZone: GYM_TZ, month: 'short', day: 'numeric' }),
        today: iso === todayIso,
        jsDate: d,
      };
    });
  }, [weekStart]);

  const [focusDay, setFocusDay] = React.useState(weekDays.find(d => d.today)?.iso || weekDays[0]?.iso);

  // Group entries by *gym-local* day. iso and minutes are both gym-time so a
  // booking at 11pm ET that's 12am next-day in the browser's TZ still lands
  // on the gym's calendar date and at the right vertical offset.
  const eventsByDay = React.useMemo(() => {
    const map = {};
    (cal.data?.entries || []).forEach(e => {
      const d = parseDate(e.date);
      if (!d) return;
      const iso = gymIsoDay(d);
      const minutes = gymMinutesOfDay(d);
      if (!iso || minutes == null) return;
      (map[iso] = map[iso] || []).push({ ...e, minutes, jsDate: d });
    });
    Object.values(map).forEach(arr => arr.sort((a, b) => a.minutes - b.minutes));
    return map;
  }, [cal.data]);

  const days = view === 'week' ? weekDays : weekDays.filter(d => d.iso === focusDay);
  const gridCols = view === 'day' ? '60px 1fr' : '60px repeat(7, 1fr)';

  // Stages that mean "still on the floor schedule" — anything else (DEAD,
  // LOST, NEW LEAD that was re-classified, etc) makes the card render in
  // the cancelled style even if the original GHL appointment status is
  // still "confirmed". Mirrors ACTIVE_BOOKING_STAGES in app/stage_log.py.
  const ACTIVE_STAGES = new Set(['DAY PASS SCHEDULED', 'SHOWED', 'JOINED', 'MEMBER']);

  const colorFor = (status, currentStage) => {
    const s = String(status || '').toLowerCase();
    const stage = String(currentStage || '').toUpperCase();
    const cancelledByGhl   = s === 'cancelled' || s === 'cancel';
    const cancelledByStage = stage && !ACTIVE_STAGES.has(stage);
    if (cancelledByGhl || cancelledByStage) return { bg:'var(--warn-soft)', fg:'var(--warn)', accent:'var(--warn)' };
    if (s === 'showed' || s === 'confirmed' || stage === 'SHOWED' || stage === 'JOINED' || stage === 'MEMBER') {
      return { bg:'var(--green-soft)', fg:'var(--green)', accent:'var(--green)' };
    }
    return { bg:'var(--signal-soft)', fg:'var(--signal-ink)', accent:'var(--signal)' };
  };

  function shiftWeek(deltaWeeks) {
    setWeekStart(new Date(weekStart.getTime() + deltaWeeks * 7 * 86400000));
  }
  function gotoToday() {
    setWeekStart(mondayOfGymWeek(new Date()));
  }

  const rangeLabel =
    weekDays[0].jsDate.toLocaleDateString(undefined, { timeZone: GYM_TZ, month:'short', day:'numeric' }) +
    ' — ' +
    weekDays[6].jsDate.toLocaleDateString(undefined, { timeZone: GYM_TZ, month:'short', day:'numeric' });

  const totalCount = (cal.data?.entries || []).length;

  return (
    <div className="app-main">
      <div className="page-hd">
        <div>
          <div className="kicker" style={{marginBottom:8}}>— Floor schedule</div>
          <h1>Calendar</h1>
          <div className="sub">Week of {rangeLabel} · {totalCount} bookings in feed</div>
        </div>
        <div className="actions">
          <div style={{display:'flex', gap:0, border:'1px solid var(--line)', borderRadius:'var(--radius-sm)', overflow:'hidden'}}>
            <button className="btn ghost sm"
              style={{borderRadius:0, background: view==='week'?'var(--ink)':'transparent', color: view==='week'?'var(--bone)':'var(--ink)'}}
              onClick={() => setView('week')}>Week</button>
            <button className="btn ghost sm"
              style={{borderRadius:0, background: view==='day'?'var(--ink)':'transparent', color: view==='day'?'var(--bone)':'var(--ink)'}}
              onClick={() => setView('day')}>Day</button>
          </div>
          <button className="btn sm" onClick={() => shiftWeek(-1)}>←</button>
          <button className="btn" onClick={gotoToday}>Today</button>
          <button className="btn sm" onClick={() => shiftWeek(1)}>→</button>
        </div>
      </div>

      <div style={{display:'flex', gap:18, marginBottom:14, fontSize:12, color:'var(--muted)'}}>
        {[
          { k:'confirmed', l:'Confirmed' },
          { k:'booked',    l:'Booked' },
          { k:'cancelled', l:'Cancelled' },
        ].map(x => {
          const c = colorFor(x.k);
          return (
            <span key={x.k} style={{display:'inline-flex', alignItems:'center', gap:6}}>
              <span style={{width:10, height:10, borderRadius:2, background: c.bg, borderLeft:'2px solid '+c.accent}} />
              {x.l}
            </span>
          );
        })}
      </div>

      <div style={{background:'#FBF9F3', border:'1px solid var(--line)', borderRadius:'var(--radius)', overflow:'hidden'}}>
        <div style={{display:'grid', gridTemplateColumns: gridCols, borderBottom:'1px solid var(--line)', background:'#F7F4EE'}}>
          <div />
          {days.map(d => (
            <div key={d.iso}
              onClick={() => { setFocusDay(d.iso); if (view === 'week') setView('day'); }}
              style={{
                padding:'12px 14px', borderLeft:'1px solid var(--line-2)', cursor:'pointer',
                background: d.today ? 'var(--signal-soft)' : 'transparent',
              }}
            >
              <div style={{fontFamily:'var(--mono)', fontSize:10, letterSpacing:'0.16em', textTransform:'uppercase', color:'var(--muted-2)'}}>{d.d}</div>
              <div style={{fontWeight:600, fontSize:15, letterSpacing:'-0.01em', marginTop:2}}>
                {d.date}
                {d.today && <span style={{marginLeft:8, fontFamily:'var(--mono)', fontSize:10, padding:'1px 6px', borderRadius:4, color:'var(--signal-ink)', background:'rgba(255,255,255,0.6)', letterSpacing:'0.08em', textTransform:'uppercase'}}>today</span>}
              </div>
            </div>
          ))}
        </div>

        <div style={{
          position:'relative', display:'grid', gridTemplateColumns: gridCols,
          height: (HOUR_END - HOUR_START) * 60 * PX_PER_MIN,
        }}>
          <div style={{borderRight:'1px solid var(--line-2)', position:'relative'}}>
            {Array.from({length: HOUR_END - HOUR_START}, (_, i) => {
              const h = HOUR_START + i;
              const ampm = h >= 12 ? 'p' : 'a';
              const h12 = ((h + 11) % 12) + 1;
              return (
                <div key={h} style={{
                  position:'absolute', top: i * 60 * PX_PER_MIN - 7, right: 8,
                  fontFamily:'var(--mono)', fontSize:10, color:'var(--muted-2)', letterSpacing:'0.04em',
                }}>{h12}{ampm}</div>
              );
            })}
          </div>

          {days.map(d => {
            const entries = eventsByDay[d.iso] || [];
            return (
              <div key={d.iso} style={{
                position:'relative',
                borderLeft:'1px solid var(--line-2)',
                background: d.today ? 'rgba(255,221,190,0.08)' : 'transparent',
              }}>
                {Array.from({length: HOUR_END - HOUR_START}, (_, i) => (
                  <div key={i} style={{
                    position:'absolute', left:0, right:0,
                    top: i * 60 * PX_PER_MIN,
                    borderTop: i === 0 ? 'none' : '1px solid var(--line-2)', height:0,
                  }} />
                ))}
                {/* Booking cutoff line at 7:30 PM. Cosmetic — GHL widget enforces
                    the real cap; backend pings Telegram if a late booking slips through. */}
                {(() => {
                  const top = (BOOKING_CUTOFF_MIN - HOUR_START * 60) * PX_PER_MIN;
                  return (
                    <div style={{
                      position:'absolute', left:0, right:0, top, height:0,
                      borderTop:'1px dashed var(--muted-2)', zIndex:1,
                    }} title="Latest booking time (7:30 PM ET)" />
                  );
                })()}
                {d.today && (() => {
                  const mins = gymMinutesOfDay(new Date());
                  if (mins == null || mins < HOUR_START*60 || mins > HOUR_END*60) return null;
                  return (
                    <div style={{
                      position:'absolute', left:0, right:0,
                      top: (mins - HOUR_START*60) * PX_PER_MIN,
                      height:1, background:'var(--signal)', zIndex: 3,
                    }}>
                      <div style={{position:'absolute', left:-4, top:-4, width:8, height:8, borderRadius:'50%', background:'var(--signal)'}} />
                    </div>
                  );
                })()}
                {entries.map((e, i) => {
                  const cls = colorFor(e.status, e.current_stage);
                  // Real duration from backend if present, else 30 min default.
                  const startMs = e.jsDate?.getTime();
                  const endMs = e.end_date ? parseDate(e.end_date)?.getTime() : null;
                  const durMin = (endMs && startMs && endMs > startMs)
                    ? Math.round((endMs - startMs) / 60000)
                    : 30;
                  const offset = Math.max(0, (e.minutes - HOUR_START*60)) * PX_PER_MIN;
                  // Min 48px so a 1-line time + 2-line name always fits without
                  // clipping at narrow column widths (when the week view squeezes
                  // each day to ~180px).
                  const height = Math.max(48, durMin * PX_PER_MIN - 3);
                  const status = e.status || 'booked';
                  // Cancelled-by-stage: contact moved out of DAY PASS SCHEDULED
                  // / SHOWED / JOINED / MEMBER after the booking. Show stage in
                  // the time line so staff knows why the card is red.
                  const stageUpper = String(e.current_stage || '').toUpperCase();
                  const cancelledByStage = stageUpper && !ACTIVE_STAGES.has(stageUpper);
                  return (
                    <div key={e.id || i}
                      onClick={() => e.contact_id && onNavigate({view:'lead', id:e.contact_id})}
                      title={`${fmtClock(e.date)} · ${cancelledByStage ? stageUpper : status} · ${e.name}`}
                      style={{
                        position:'absolute', left:4, right:4,
                        top:offset, height,
                        background:cls.bg, borderLeft:'2px solid ' + cls.accent,
                        borderRadius:4, padding:'5px 7px', fontSize:11.5,
                        overflow:'hidden',
                        display:'flex', flexDirection:'column', gap:2,
                        cursor: e.contact_id ? 'pointer' : 'default', zIndex:2,
                      }}
                    >
                      <div style={{
                        fontFamily:'var(--mono)', fontSize:9, letterSpacing:'0.1em',
                        textTransform:'uppercase', color:cls.fg, opacity:0.85,
                        whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis',
                        flex:'0 0 auto',
                      }}>
                        {fmtClock(e.date)}{cancelledByStage ? ' · ' + stageUpper : ''}
                      </div>
                      <div style={{
                        fontWeight:500, color:'var(--ink)', lineHeight:1.25,
                        display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical',
                        overflow:'hidden', wordBreak:'break-word',
                        flex:'1 1 auto', minHeight:0,
                        textDecoration: cancelledByStage ? 'line-through' : 'none',
                        opacity: cancelledByStage ? 0.65 : 1,
                      }}>
                        {e.name}
                      </div>
                    </div>
                  );
                })}
                {entries.length === 0 && d.today && (
                  <div style={{position:'absolute', bottom:8, left:8, right:8, fontSize:11, color:'var(--muted-2)', fontFamily:'var(--mono)'}}>
                    no bookings today
                  </div>
                )}
              </div>
            );
          })}
        </div>
      </div>

      {totalCount === 0 && !cal.loading && (
        <div style={{marginTop:16}}>
          <div className="empty">No bookings yet. Appointments booked via GHL will appear here automatically.</div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { CalendarView });
