// Supabase Edge Function: mindbody-sync
// Pulls appointments + active pricing options from Mindbody Public API v6 and upserts roster_clients.
// Secrets required: MB_API_KEY, MB_SITE_ID, MB_STAFF_USER, MB_STAFF_PASS (SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY are provided automatically).
// Body: { "days": 7 }  — lookback window for appointments. Use 260 on the first run to load the whole year.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

const MB = "https://api.mindbodyonline.com/public/v6";
const env = (k: string) => Deno.env.get(k) ?? "";

const locOf = (name: string) => { const n = (name || "").toLowerCase(); if (/belltown/.test(n)) return "Belltown"; if (/lake|slu/.test(n)) return "SLU"; if (/madison/.test(n)) return "MP"; return null; };

function planOf(name: string) {
  const n = name || "";
  if (/nco|intro/i.test(n)) return { plan: "NCO in progress", monthly: false, nco: true };
  const partner = /partner|semi/i.test(n), thirty = /30/.test(n);
  const pfx = partner ? "Partner" : thirty ? "PT 30" : "PT 55";
  if (/single session/i.test(n)) return { plan: `${pfx} · single session`, monthly: false };
  let m = /(\d+)\s+(?:personal|partner|semi[- ]private)\s+training\s+sessions/i.exec(n);
  if (m) return { plan: `${pfx} · ${m[1]} / month`, monthly: true };
  m = /(\d+)\s+sessions?\s+pack/i.exec(n);
  if (m) return { plan: `${pfx} · ${m[1]} pack`, monthly: false };
  if (/check[- ]?in/i.test(n)) return { plan: "Monthly check-in", monthly: true };
  return { plan: n, monthly: false };
}
function gymOf(name: string) {
  const n = (name || "").toLowerCase(); const couple = /couple|duo|family/.test(n);
  if (/standard plus|coached/.test(n)) return couple ? "Standard Plus · Couple" : "Standard Plus · Individual";
  if (/annual/.test(n)) return couple ? "Open gym · Couple annual" : "Open gym · Individual annual";
  return couple ? "Open gym · Couple" : "Open gym · Individual";
}

Deno.serve(async (req) => {
  const sb = createClient(env("SUPABASE_URL"), env("SUPABASE_SERVICE_ROLE_KEY"));
  const body = await req.json().catch(() => ({}));
  const days = Math.min(400, Number(body.days) || 7);
  const offset = Math.max(0, Number(body.offset) || 0); // days back from today where the window ends
  const t0 = Date.now(), now = Date.now();
  try {
    const hdr = { "Api-Key": env("MB_API_KEY"), "SiteId": env("MB_SITE_ID"), "Content-Type": "application/json" };
    const tokRes = await fetch(`${MB}/usertoken/issue`, { method: "POST", headers: hdr, body: JSON.stringify({ Username: env("MB_STAFF_USER"), Password: env("MB_STAFF_PASS") }) });
    const tok = await tokRes.json();
    if (!tok.AccessToken) throw new Error("Mindbody login failed: " + JSON.stringify(tok).slice(0, 300));
    const H = { ...hdr, Authorization: tok.AccessToken };
    const get = async (path: string, params: Record<string, unknown> = {}) => {
      const u = new URL(MB + path);
      for (const [k, v] of Object.entries(params)) { if (Array.isArray(v)) v.forEach((x) => u.searchParams.append(k, String(x))); else if (v != null) u.searchParams.set(k, String(v)); }
      const r = await fetch(u, { headers: H }); const j = await r.json();
      if (!r.ok) throw new Error(`${path} ${r.status} ${JSON.stringify(j).slice(0, 300)}`);
      return j;
    };
    const pageAll = async (path: string, params: Record<string, unknown>, key: string) => {
      let out: any[] = [], off = 0;
      while (true) { const j = await get(path, { ...params, Limit: 200, Offset: off }); const arr = j[key] || []; out = out.concat(arr); const p = j.PaginationResponse; if (!arr.length || !p || off + arr.length >= p.TotalResults) break; off += arr.length; }
      return out;
    };

    const locMap: Record<string, string | null> = {}; ((await get("/site/locations")).Locations || []).forEach((l: any) => { locMap[l.Id] = locOf(l.Name); });
    const staffMap: Record<string, string> = {}; (await pageAll("/staff/staff", {}, "StaffMembers")).forEach((s: any) => { staffMap[s.Id] = (s.FirstName || "").trim(); });

    const end = new Date(now - offset * 86400000 + 86400000), start = new Date(now - (offset + days) * 86400000);
    const appts = await pageAll("/appointment/staffappointments", { StartDate: start.toISOString().slice(0, 10), EndDate: end.toISOString().slice(0, 10) }, "Appointments");

    const byClient: Record<string, any> = {};
    for (const a of appts) {
      const st = String(a.Status || ""); const when = String(a.StartDateTime || "");
      if (!a.ClientId || new Date(when).getTime() > now) continue;
      if (/cancel|noshow|no show/i.test(st) && !/late/i.test(st)) continue;
      const c = (byClient[a.ClientId] ||= { visits: 0, late: 0, last: null as string | null, staffId: null, locId: null });
      if (/late/i.test(st)) { c.late++; continue; }
      c.visits++; if (!c.last || when > c.last) { c.last = when; c.staffId = a.StaffId; c.locId = a.LocationId; }
    }
    const ids = Object.keys(byClient);
    const cmap: Record<string, any> = {};
    for (let i = 0; i < ids.length; i += 20) { const j = await get("/client/clients", { ClientIds: ids.slice(i, i + 20), Limit: 20 }); (j.Clients || []).forEach((c: any) => { cmap[c.Id] = c; }); }

    const { data: existing, error: exErr } = await sb.from("roster_clients").select("id,mb_client_id,name,trainer,status_locked,last_visit,handoff");
    if (exErr) throw exErr;
    const byMb: Record<string, any> = {}, byName: Record<string, any> = {};
    (existing || []).forEach((r: any) => { if (r.mb_client_id) byMb[r.mb_client_id] = r; byName[`${(r.name || "").trim().toLowerCase()}|${r.trainer}`] = r; });

    // fetch active pricing options in parallel (5 at a time) for clients seen in the last 90 days
    const svcMap: Record<string, any[]> = {};
    const needSvc = ids.filter((id) => { const c = byClient[id]; return c.last && (now - new Date(c.last).getTime()) / 86400000 <= 90; });
    for (let i = 0; i < needSvc.length; i += 5) {
      await Promise.all(needSvc.slice(i, i + 5).map(async (id) => { try { svcMap[id] = (await get("/client/clientservices", { ClientId: id, ShowActiveOnly: true })).ClientServices || []; } catch (_e) { svcMap[id] = []; } }));
    }

    let created = 0, updated = 0, skipped = 0;
    for (const id of ids) {
      const c = byClient[id], info = cmap[id]; if (!info) { skipped++; continue; }
      const name = `${info.FirstName || ""} ${info.LastName || ""}`.trim(); if (!name || /^test/i.test(name)) continue;
      const trainer = staffMap[c.staffId] || ""; if (!trainer) { skipped++; continue; }
      const loc = locMap[c.locId] || "Belltown";
      const lastDate = c.last ? c.last.slice(0, 10) : null;
      const ex = byMb[String(id)] || byName[`${name.toLowerCase()}|${trainer}`];
      if (ex && ex.last_visit && lastDate && ex.last_visit > lastDate) { skipped++; continue; }
      const daysSince = c.last ? Math.round((now - new Date(c.last).getTime()) / 86400000) : 999;
      const svc: any[] = svcMap[id] || [];
      svc.sort((a, b) => String(b.PaymentDate || b.ActiveDate || "").localeCompare(String(a.PaymentDate || a.ActiveDate || "")));
      const train = svc.find((s) => /training|session|nco|intro|check/i.test(s.Name || "") && !/gym|member/i.test(s.Name || ""));
      const gym = svc.find((s) => /gym|member|standard plus|coached/i.test(s.Name || ""));
      const p = train ? planOf(train.Name) : null;
      const status = daysSince <= 30 ? "active" : daysSince <= 90 ? "paused" : "ended";
      const row: Record<string, unknown> = { name, location: loc, trainer, mb_client_id: String(id), email: info.Email || null, last_visit: lastDate, synced_at: new Date().toISOString(), updated_by: "mindbody-sync" };
      if (c.late) row.late_cancels = c.late;
      if (p) { row.plan = p.plan; row.plan_monthly = p.monthly; row.sessions_left = train.Remaining ?? null; row.plan_expires = train.ExpirationDate ? String(train.ExpirationDate).slice(0, 10) : null; row.last_purchase = train.PaymentDate ? String(train.PaymentDate).slice(0, 10) : null; }
      if (gym) row.gym = gymOf(gym.Name);
      if (ex) { if (!ex.status_locked) row.status = status; if (ex.handoff) { delete row.trainer; delete row.location; } const { error } = await sb.from("roster_clients").update(row).eq("id", ex.id); if (error) throw error; updated++; }
      else { row.status = status; row.source = p?.nco ? "NCO" : "Legacy"; row.started = c.last ? c.last.slice(0, 10) : null; const { error } = await sb.from("roster_clients").insert(row); if (error) throw error; created++; }
    }
    const d30 = new Date(now - 30 * 86400000).toISOString().slice(0, 10), d90 = new Date(now - 90 * 86400000).toISOString().slice(0, 10);
    await sb.from("roster_clients").update({ status: "paused" }).eq("status", "active").eq("status_locked", false).not("mb_client_id", "is", null).lt("last_visit", d30);
    await sb.from("roster_clients").update({ status: "ended" }).eq("status", "paused").eq("status_locked", false).not("mb_client_id", "is", null).lt("last_visit", d90);

    const summary = `${appts.length} appointments · ${ids.length} clients · ${created} new · ${updated} updated · ${skipped} skipped · ${Math.round((Date.now() - t0) / 1000)}s · window ${days}d ending ${offset}d ago`;
    await sb.from("sync_log").insert({ ok: true, summary });
    return Response.json({ ok: true, summary });
  } catch (e) {
    const msg = String((e as any)?.message || e);
    await sb.from("sync_log").insert({ ok: false, summary: msg });
    return Response.json({ ok: false, error: msg }, { status: 500 });
  }
});
