// SalesStrip.jsx — the always-on sales pace bar, below the tab bar (v16.00).
//
// What it measures is CREW LOAD: how busy the day is against the crew that
// was put on it. 100% is the sales level at which the day's scheduled labor
// lands exactly on that weekday's target share. Under 100% we staffed for more
// business than showed up — there is slack, and prep should get done. Over
// 100% the business outran the crew, which is what waives incomplete prep.
//
// It is NOT efficiency (that would be a rate — sales per labor dollar) and it
// is NOT a plain sales goal; both names shipped and both were wrong (v16.04,
// v16.06). Named "crew load" as of v16.08.
//
// Deliberately NOT a raw progress bar either. $900 of $2,000 at 2pm means
// "comfortably ahead" on a Saturday and "we're dead" on a Tuesday — the same
// bar giving opposite instructions is how a strip gets ignored inside a
// fortnight. The hourly shape drawn on the portal’s Sales vs. Labor page is
// what turns it into pace.
//
// The goal comes from the day's SCHEDULED labor (wages + full bonus, LESS any
// training labor keyed in on the portal) divided by that weekday's target,
// both snapshotted at open, so it's fixed all day and can't drift under the
// crew's feet.
//
// Data is read, never computed here: sales_daily is written every 5 minutes by
// the sales-rollup cron (from 08:00 local), labor_daily once at 07:00 so the
// goal is up before the first shift. This polls and draws, and self-heals if
// a day somehow has no rows yet.
//
// Visual treatment (v16.02 "Quiet", restructured v16.04): the whole band is
// the progress bar — a pale wash fills it left-to-right behind the text, like
// a Side Job card. Reported as a PERCENTAGE of the day's goal, never in
// dollars: this sits on the line in front of the whole crew, and "are we on
// track" is the question worth answering there.
//
// Idents are ss-prefixed — Babel-standalone shares one global scope (§9).

const { useState: useSs, useEffect: useSsEffect } = React;

const SS_POLL_MS = 5 * 60 * 1000;   // matches the sales-rollup cron
const SS_ROUND = 25;                // goal rounded to the nearest $25

const ssToday = () => {
  const d = new Date();
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
};

const ssMoney = (dollars) => '$' + Math.round(dollars).toLocaleString();

// Share of the day that should be banked by `now`, from the hand-drawn curve.
// Within the current hour we interpolate by minutes, otherwise the bar would
// jump a whole block on the hour and read as a sudden collapse in pace.
const ssExpectedShare = (blocks, openHour, now) => {
  const total = blocks.reduce((s, b) => s + b, 0);
  if (!total) return null;                     // no curve drawn → no pace claim
  const h = now.getHours(), m = now.getMinutes();
  let acc = 0;
  for (let i = 0; i < blocks.length; i++) {
    const hourOfBlock = openHour + i;
    if (h > hourOfBlock) acc += blocks[i];
    else if (h === hourOfBlock) acc += blocks[i] * (m / 60);
  }
  return Math.min(1, acc / total);
};

const SalesStrip = () => {
  const [state, setState] = useSs(null);
  // One self-heal attempt per mount. The crons own the happy path; this is
  // only for the gap where a day has no rows yet — first iPad awake before the
  // cron fired, or a cron that missed. Without it the strip sits on "no goal
  // set for today", which reads as broken rather than as not-yet.
  const healedRef = React.useRef(false);

  useSsEffect(() => {
    if (!window.supa || !window.RESTAURANT_ID) return;
    let cancelled = false;

    const load = async (allowHeal) => {
      const day = ssToday();
      const [rest, sales, labor, untrack, train] = await Promise.all([
        window.supa.from('restaurants').select('sales_goal_config').eq('id', window.RESTAURANT_ID).maybeSingle(),
        window.supa.from('sales_daily').select('labor_sales_cents').eq('restaurant_id', window.RESTAURANT_ID).eq('the_date', day).maybeSingle(),
        window.supa.from('labor_daily').select('scheduled_cost_cents').eq('restaurant_id', window.RESTAURANT_ID).eq('the_date', day).maybeSingle(),
        window.supa.from('sales_untrackable_days').select('the_date').eq('restaurant_id', window.RESTAURANT_ID).eq('the_date', day).maybeSingle(),
        window.supa.from('labor_training').select('amount_cents').eq('restaurant_id', window.RESTAURANT_ID).eq('the_date', day).maybeSingle(),
      ]);
      if (cancelled) return;

      // Nothing for today yet → ask the functions to build it, once, then
      // re-read. Both are safe to call: labor-schedule returns early on a
      // locked day and sales-rollup upserts.
      if (allowHeal && !healedRef.current && !labor.data && window.callEdge) {
        healedRef.current = true;
        await Promise.all([
          window.callEdge('labor-schedule', {}),
          window.callEdge('sales-rollup', {}),
        ]);
        if (!cancelled) load(false);
        return;
      }

      setState({
        cfg: (rest.data && rest.data.sales_goal_config) || {},
        // labor_sales_cents, NOT net_cents (v19.09): gross minus third-party
        // commission, with discounts left IN. A discounted bento is the same
        // food cooked in the same minutes, so docking the line for a pricing
        // decision they had no part in measured the wrong thing. The
        // commission stays out — that money genuinely never arrives.
        // scorecard_snapshot_range's waiver reads the same column; if these
        // two ever diverge the crew gets told they hit target while the
        // scorecard quietly says they didn't (§9).
        netCents: (sales.data && sales.data.labor_sales_cents) || 0,
        // Training labor is netted off (v16.11). A new hire shadowing is not
        // production, so leaving it in would raise the goal and make the crew
        // look slow for a decision that wasn't theirs. Same subtraction runs
        // in the portal cards and in scorecard_snapshot_range's waiver —
        // all three read this table, or they disagree (§9).
        laborCents: Math.max(0,
          ((labor.data && labor.data.scheduled_cost_cents) || 0)
          - ((train.data && train.data.amount_cents) || 0)),
        untrackable: !!(untrack.data && untrack.data.the_date),
        at: Date.now(),
      });
    };

    load(true);
    // Reset the heal guard each poll cycle so a day rolling over at midnight
    // can heal itself too, not just a fresh page load.
    const t = setInterval(() => { healedRef.current = false; load(true); }, SS_POLL_MS);
    return () => { cancelled = true; clearInterval(t); };
  }, []);

  if (!state) return null;

  // v16.04 — the strip IS the bar. The whole band fills left-to-right as a
  // tinted wash behind the text, the way a Side Job card does, with the
  // reading as a percentage of the day's goal. No dollars: this screen is on
  // the line where the whole crew and anyone leaning over the pass can read
  // it, and "are we on track" is the question — the day's revenue is not
  // theirs to carry around.
  //
  // Colour still only appears in the wash and the words (the v16.02 "Quiet"
  // decision); a solid saturated band all day would burn the signal we want
  // for goal-hit.
  // Text sits ON the wash, so contrast is measured against the tinted band,
  // not white. The first cut used the standard muted grey and emerald-700,
  // which came out at 4.12:1 and 4.67:1 over a 13%-alpha green — under, and
  // barely over, the 4.5:1 floor. One stop darker each puts every combination
  // above 6:1, which is what you want on a screen read at arm's length across
  // a hot line.
  const SS_C = {
    ahead:  { fill: '#059669', text: '#065F46' },   // 6.54:1 on its wash
    behind: { fill: '#D97706', text: '#92400E' },   // 6.16:1
    onpace: { fill: '#52525B', text: '#3F3F46' },
  };
  const SS_LABEL = '#52525B';                        // 6.58:1 on the green wash
  const SS_PAD = {
    padding: '13px 16px calc(13px + env(safe-area-inset-bottom))',
    paddingLeft: 'calc(16px + env(safe-area-inset-left))',
    paddingRight: 'calc(16px + env(safe-area-inset-right))',
  };

  // States with nothing to chart — plain band, no fill.
  const plain = (children) => (
    <div style={{
      ...SS_PAD, display: 'flex', alignItems: 'center', gap: 12,
      background: '#FFFFFF', borderTop: '1px solid var(--border-1, #E4E4E7)',
      flexShrink: 0, fontSize: 13,
    }}>{children}</div>
  );

  if (state.untrackable) {
    return plain(<span style={{ color: SS_LABEL, fontWeight: 500 }}>Untrackable sales today</span>);
  }

  const cfg = state.cfg || {};
  const openHour = Number(cfg.open_hour) || 11;
  const closeHour = Number(cfg.close_hour) || 22;
  const dow = new Date().getDay();
  const targetPct = cfg.targets && cfg.targets[String(dow)] !== undefined
    ? Number(cfg.targets[String(dow)]) : 25;

  // No schedule snapshot even after the self-heal above means Square has
  // nothing published for today. There is no goal to be a percentage OF.
  if (!state.laborCents || targetPct <= 0) {
    return plain(
      <span style={{ color: SS_LABEL }}>Goal pending — no schedule published for today</span>);
  }

  const goal = Math.round(((state.laborCents / 100) / (targetPct / 100)) / SS_ROUND) * SS_ROUND;
  const banked = state.netCents / 100;
  const pct = goal > 0 ? (banked / goal) * 100 : 0;
  const fillPct = Math.min(100, pct);

  const hours = [];
  for (let h = openHour; h < closeHour; h++) hours.push(h);
  const blocks = hours.map((_, i) => Number(((cfg.curve || {})[String(dow)] || [])[i]) || 0);
  const expected = ssExpectedShare(blocks, openHour, new Date());

  const hit = pct >= 100;
  let c = SS_C.onpace, verdict = null;
  if (hit) {
    c = SS_C.ahead;
    verdict = 'Goal hit';
  } else if (expected !== null) {
    // A 5% dead-band around the line: without it the wording flickers between
    // "ahead" and "behind" on single orders, which reads as noise.
    const expectedPct = expected * 100;
    const ratio = expectedPct > 0 ? pct / expectedPct : 1;
    if (ratio >= 1.05) { c = SS_C.ahead; verdict = 'Ahead of pace'; }
    else if (ratio <= 0.95) { c = SS_C.behind; verdict = 'Behind pace'; }
    else { c = SS_C.onpace; verdict = 'On pace'; }
  }

  return (
    <div style={{
      position: 'relative', overflow: 'hidden', flexShrink: 0,
      background: '#FFFFFF', borderTop: '1px solid var(--border-1, #E4E4E7)',
    }}>
      {/* The wash. Alpha-suffixed so the text on top stays readable at any
          fill width — a solid fill would swallow the number as it passed. */}
      <div style={{
        position: 'absolute', left: 0, top: 0, bottom: 0, width: fillPct + '%',
        background: c.fill + (hit ? '2E' : '22'),
        transition: 'width 400ms ease-out',
      }} />
      {/* Where the day's shape says we should be by now. Full height so it
          reads as a gate the fill is racing, not a tick on a rule. */}
      {expected !== null && !hit && (
        <div style={{
          position: 'absolute', top: 0, bottom: 0,
          left: 'calc(' + (expected * 100) + '% - 1px)',
          width: 2, background: 'rgba(0,0,0,0.28)',
        }} />
      )}
      <div style={{
        ...SS_PAD, position: 'relative',
        display: 'flex', alignItems: 'baseline', gap: 12, fontSize: 15,
      }}>
        <span style={{
          fontSize: 22, fontWeight: 700, fontFamily: 'var(--font-num)',
          color: c.text, lineHeight: 1, whiteSpace: 'nowrap',
        }}>
          {hit && <i className="ri-check-line" style={{ marginRight: 5, verticalAlign: '-1px' }} />}
          {Math.round(pct)}%
        </span>
        <span style={{ color: SS_LABEL, whiteSpace: 'nowrap' }}>crew load</span>
        <span style={{ flex: 1 }} />
        {verdict && (
          <span style={{ whiteSpace: 'nowrap', fontWeight: 600, color: c.text }}>{verdict}</span>
        )}
      </div>
    </div>
  );
};

Object.assign(window, { SalesStrip });
