/* global React, sbx, saveCourse */
// Onboarding a course — what is filled in, what is left, and how far along.
//
// There is no onboarding table and deliberately so: the draft `courses` row IS
// the unfinished onboarding. Progress is derived from the records the steps
// write, which means it cannot drift out of step with reality, it survives
// anything (another admin, another browser, a year later), and resuming is just
// opening the course again.
//
// This is separate from the server's readiness checklist (`course_readiness`,
// see courses-data.jsx). The two answer different questions — "how much of the
// pack is filled in" against "what does the database require before it will let
// this go live" — and they are shown separately for that reason. Folding them
// together would let the bar read 100% while activate_course still refuses.

// ─── The checklist ───────────────────────────────────────────────────────────
// Every item is a predicate over data the admin already loads. `blocks: false`
// marks the ones that are worth doing but do not stand between a course and
// going live, so the bar can be honest without being a nag.

// "Has a horizon" rather than "has any future slot at all". A course whose
// only published times are three months out is not operational, and a bounded
// window is also what lets the whole grid be costed in one query instead of
// one per course — so the card and the course page count the same thing.
const SLOT_HORIZON_DAYS = 14;
const horizonEnd = () => new Date(Date.now() + SLOT_HORIZON_DAYS * 864e5).toISOString();

const has = (v) => v != null && String(v).trim() !== '';
const num = (v) => v != null && v !== '' && Number(v) > 0;

const ONBOARDING_PHASES = [
  {
    id: 'identity',
    label: 'Identity & place',
    hint: 'What the course is called and where it is',
    items: [
      { id: 'name',     label: 'Course name',        test: (d) => has(d.course.name) },
      { id: 'short',    label: 'Short name',         test: (d) => has(d.course.short_name),
        hint: 'What the golfer app shows on a card' },
      { id: 'where',    label: 'City & state',       test: (d) => has(d.course.city) && has(d.course.state) },
      { id: 'address',  label: 'Street address',     test: (d) => has(d.course.address) },
      { id: 'phone',    label: 'Phone',              test: (d) => has(d.course.phone), blocks: false },
      { id: 'pin',      label: 'Map coordinate',     test: (d) => d.course.lat != null && d.course.lng != null,
        hint: 'Look it up from the address, or drag the pin' },
      { id: 'photo',    label: 'A photograph',       test: (d) => has(d.course.hero_img) || has(d.course.render_img),
        hint: 'Hero shot or drawn flyover — either fills the card', blocks: false },
    ],
  },
  {
    id: 'layout',
    label: 'Scorecard & layout',
    hint: 'The real course, and the Sandbox 9 laid over it',
    items: [
      { id: 'rc',       label: 'Real scorecard on file', test: (d) => !!d.rc,
        hint: 'Import it from BlueGolf, or type it in' },
      { id: 'tees',     label: 'At least one tee',       test: (d) => (d.rcTees || []).length > 0 },
      { id: 'holes',    label: 'Nine holes with a par',  test: (d) => nineWith(d.holes, (h) => num(h.par)) },
      { id: 'yards',    label: 'Sandbox yardages',       test: (d) => nineWith(d.holes, (h) => num(h.sandbox_yards)) },
      { id: 'quads',    label: 'Pin quadrants measured', test: (d) => nineWith(d.holes, hasQuadrants),
        hint: 'Four measured pin positions per hole' },
    ],
  },
  {
    id: 'commercials',
    label: 'Commercials',
    hint: 'What it costs and what we keep',
    items: [
      { id: 'price',    label: 'Walk-up price',      test: (d) => num(d.course.suggested_price) },
      { id: 'split',    label: 'Sandbox take',       test: (d) => d.course.sandbox_take_pct != null },
      { id: 'tier',     label: 'Partnership tier',   test: (d) => has(d.course.partnership_tier) },
      { id: 'contract', label: 'Contract start',     test: (d) => has(d.course.contract_start) },
    ],
  },
  {
    id: 'access',
    label: 'Access & operations',
    hint: 'Who runs it, when it is playable, how it gets paid',
    items: [
      { id: 'manager',  label: 'A course manager',   test: (d) => (d.managers || []).length > 0,
        hint: 'Someone with partner-portal access' },
      { id: 'slots',    label: 'Tee times published', test: (d) => (d.futureSlots || 0) > 0,
        hint: `At least one bookable time in the next ${SLOT_HORIZON_DAYS} days` },
      { id: 'stripe',   label: 'Stripe connected',   test: (d) => has(d.course.stripe_account_id),
        // No "doesn't block going live" here: CheckRow appends that itself for
        // anything with blocks:false, and saying it twice on one line is how it
        // read on screen.
        hint: 'Needed to pay the course', blocks: false },
    ],
  },
];

// Nine holes, numbered 1-9, each satisfying the predicate. Holes past 9 are
// ignored: the Sandbox layout is a nine whatever the real course is.
function nineWith(holes, ok) {
  const byNumber = new Map((holes || []).map((h) => [Number(h.hole_number), h]));
  for (let n = 1; n <= 9; n++) {
    const h = byNumber.get(n);
    if (!h || !ok(h)) return false;
  }
  return true;
}

// quadrant_yards is jsonb — shape has varied, so this accepts an array of four
// or an object keyed by quadrant, and asks only that four numbers are present.
function hasQuadrants(h) {
  const q = h && h.quadrant_yards;
  if (!q) return false;
  const values = Array.isArray(q) ? q : Object.values(q);
  return values.filter((v) => v != null && v !== '' && Number(v) > 0).length >= 4;
}

// ─── Progress ────────────────────────────────────────────────────────────────
// Pure, so it can be tested against fixtures without a database — which is the
// only way it gets tested at all from here.
//
//   data: { course, holes, rc, rcTees, managers, futureSlots }
//   →  { phases: [{ id, label, hint, items: [{…, done }], done, total, pct }],
//        done, total, pct, blockingLeft, nextIncomplete }
function onboardingProgress(data) {
  const d = {
    course: (data && data.course) || {},
    holes: (data && data.holes) || [],
    rc: (data && data.rc) || null,
    rcTees: (data && data.rcTees) || [],
    managers: (data && data.managers) || [],
    futureSlots: (data && data.futureSlots) || 0,
  };

  const phases = ONBOARDING_PHASES.map((p) => {
    const items = p.items.map((it) => {
      let done = false;
      // A predicate should never take the page down with it — a missing
      // column or an unexpected shape reads as "not done", not as a crash.
      try { done = !!it.test(d); } catch (e) { done = false; }
      return { id: it.id, label: it.label, hint: it.hint || null, blocks: it.blocks !== false, done };
    });
    const done = items.filter((i) => i.done).length;
    return {
      id: p.id, label: p.label, hint: p.hint, items,
      done, total: items.length,
      pct: items.length ? Math.round((done / items.length) * 100) : 0,
    };
  });

  const all = phases.flatMap((p) => p.items.map((i) => ({ ...i, phase: p.id })));
  const done = all.filter((i) => i.done).length;
  const firstOpen = all.find((i) => !i.done) || null;

  return {
    phases,
    done,
    total: all.length,
    pct: all.length ? Math.round((done / all.length) * 100) : 0,
    blockingLeft: all.filter((i) => !i.done && i.blocks).length,
    nextIncomplete: firstOpen ? { phase: firstOpen.phase, item: firstOpen.id } : null,
  };
}

// Is this course mid-onboarding? An active course is done with this screen
// whatever the checklist says, and a course with nothing filled in has not
// started. Used to decide whether a card shows a progress bar.
function isOnboarding(course, pct) {
  if (!course || !course.id) return false;
  if (course.status === 'active') return false;
  return pct == null || pct < 100;
}

// ─── Loading ─────────────────────────────────────────────────────────────────
// One hook, four reads. `tee_slots` is a head count rather than the rows —
// there can be thousands and all we need is whether any are ahead of today.
function useOnboarding(courseId) {
  const [data, setData] = React.useState(null); // null = loading
  const load = React.useCallback(async () => {
    if (!courseId) { setData(null); return; }
    const [holes, managers, slots] = await Promise.all([
      sbx.from('course_holes').select('hole_number, par, sandbox_yards, quadrant_yards').eq('course_id', courseId),
      sbx.from('course_managers').select('id').eq('course_id', courseId),
      sbx.from('tee_slots').select('id', { count: 'exact', head: true })
        .eq('course_id', courseId)
        .gte('starts_at', new Date().toISOString()).lte('starts_at', horizonEnd()),
    ]);
    setData({
      holes: holes.data || [],
      managers: managers.data || [],
      // A failed count reads as none rather than as a crash; the step just
      // shows as outstanding, which is the safe way round.
      futureSlots: slots.count || 0,
    });
  }, [courseId]);
  React.useEffect(() => { load(); }, [load]);
  return [data, load];
}

// ─── The whole grid at once ──────────────────────────────────────────────────
// Three queries for every course on the page, not three per course. Holes are
// a dozen skinny rows each; managers are one row per link; slots are bounded by
// the horizon window above, which is the whole reason that window exists —
// unbounded, a single course with a year published would be thousands of rows.
function useOnboardingAll(courseIds) {
  const [byCourse, setByCourse] = React.useState(null);
  const key = (courseIds || []).slice().sort().join(',');

  const load = React.useCallback(async () => {
    const ids = key ? key.split(',') : [];
    if (!ids.length) { setByCourse({}); return; }
    const [holes, managers, slots] = await Promise.all([
      sbx.from('course_holes').select('course_id, hole_number, par, sandbox_yards, quadrant_yards').in('course_id', ids),
      sbx.from('course_managers').select('course_id').in('course_id', ids),
      sbx.from('tee_slots').select('course_id').in('course_id', ids)
        .gte('starts_at', new Date().toISOString()).lte('starts_at', horizonEnd()),
    ]);
    const out = {};
    for (const id of ids) out[id] = { holes: [], managers: [], futureSlots: 0 };
    for (const h of (holes.data || [])) if (out[h.course_id]) out[h.course_id].holes.push(h);
    for (const m of (managers.data || [])) if (out[m.course_id]) out[m.course_id].managers.push(m);
    for (const t of (slots.data || [])) if (out[t.course_id]) out[t.course_id].futureSlots += 1;
    setByCourse(out);
  }, [key]);

  React.useEffect(() => { load(); }, [load]);
  return [byCourse, load];
}

// Which real courses have at least one tee on them. One query over a small
// table, shared by the grid and the course page so the two cannot disagree
// about whether the scorecard step is done.
function useRcTeeCourseIds() {
  const [ids, setIds] = React.useState(null);
  const load = React.useCallback(async () => {
    const { data } = await sbx.from('rc_tees').select('course_id');
    setIds(new Set((data || []).map((t) => t.course_id)));
  }, []);
  React.useEffect(() => { load(); }, [load]);
  return [ids, load];
}

// The shape onboardingProgress wants for its tee check, from that set.
const teesFor = (rc, teeIds) => (rc && teeIds && teeIds.has(rc.id)) ? [{ id: rc.id }] : [];

// ─── Starting one ────────────────────────────────────────────────────────────
// Creates the `courses` row for a course we hold a scorecard for, or a wholly
// new one, and leaves it as a draft.
//
// It deliberately does NOT publish. coming_soon is golfer-facing — "visible,
// badged Coming Soon" — and a course a minute into onboarding has no holes, no
// price and no photograph, so starting one used to put exactly that in front of
// golfers as a side effect of pressing a button. Publishing is now its own act
// at the end of the identity phase (ComingSoonCard, screens/onboarding.jsx),
// and CourseStatusCard can still do it by hand at any point.
async function beginOnboarding({ name, short_name, city, state }) {
  const id = await saveCourse(
    {
      name,
      short_name: short_name || String(name || '').split(/[—–-]/)[0].trim().slice(0, 24) || name,
      city: city || '',
      state: state || 'FL',
      suggested_price: 0,
      sandbox_take_pct: 15,
    },
    // No holes yet — the layout step writes them. saveCourse derives the
    // course's hole count and par from this array, so an empty one leaves
    // both at zero, which is exactly what "not laid out yet" should read as.
    []
  );
  return id;
}

Object.assign(window, {
  ONBOARDING_PHASES, onboardingProgress, isOnboarding, useOnboarding, useOnboardingAll,
  useRcTeeCourseIds, teesFor, beginOnboarding, SLOT_HORIZON_DAYS,
});
