// store.jsx — Haven live data store (Supabase-native).
// Single source of truth. Auth via Supabase; every domain read goes through
// `Db` (RLS-gated select) and every mutation through `Rpc` (SECURITY DEFINER
// functions in the `haven` schema) — see config.jsx. No more PIN, no more
// localStorage document, no more NAS.
//
// Money: the backend stores CENTS; this store converts to DOLLARS on load
// (and back on write) so screens/flows — written for the old dollar-based
// shape — don't need to change for this pass.

const centsToDollars = (c) => (Number(c) || 0) / 100;
const dollarsToCents = (d) => Math.round((parseFloat(d) || 0) * 100);

const CAT_STYLE = {
  core:     { color: 'var(--core)',     soft: 'var(--core-soft)' },
  capacity: { color: 'var(--capacity)', soft: 'var(--capacity-soft)' },
  capital:  { color: 'var(--capital)',  soft: 'var(--capital-soft)' },
};

// ── shape raw haven.get_state() JSON into the legacy dollar-based doc shape ──
function shapeBudget(cats) {
  return (cats || []).map((c) => {
    const style = CAT_STYLE[c.key] || {};
    const subs = (c.subs || []).map((s) => {
      const budget = centsToDollars(s.budget_cents), spent = centsToDollars(s.spent_cents);
      return { id: s.id, name: s.name, budget, spent, remaining: budget - spent, pct: budget ? spent / budget : 0 };
    });
    const budget = centsToDollars(c.budget_cents), spent = centsToDollars(c.spent_cents);
    return { key: c.key, name: c.name, plain: c.plain, ...style,
      budget, spent, remaining: budget - spent, pct: budget ? spent / budget : 0, subs };
  });
}
const shapeProvider = (p) => ({ id: p.id, name: p.name, short: p.short_name, cat: p.cat_key, sub: p.sub_name,
  rate: p.rate_text, agreementEnds: p.agreement_ends, agreementFile: p.agreement_file });
const shapeWorker = (w) => ({ id: w.id, name: w.name, role: w.role, rate: centsToDollars(w.rate_cents), initials: w.initials, cat: w.cat_key });
const shapeTimesheet = (t) => ({ id: t.id, worker: t.worker_id, date: t.work_date, hours: Number(t.hours), note: t.note, status: t.status });
const shapeClaim = (c) => ({ id: c.id, provider: c.provider, cat: c.cat_key, sub: c.sub_name, date: c.service_date,
  amount: centsToDollars(c.amount_cents), status: c.status, submitted: c.submitted_date,
  paid: c.paid_date, invoice: c.invoice_ref, reason: c.reason });
const shapeInvoice = (i) => ({ id: i.id, ref: i.ref, provider: i.provider, cat: i.cat_key, date: i.doc_date,
  amount: centsToDollars(i.amount_cents), kind: i.kind, claimed: i.claimed_claim_id, file: i.file });
const shapeGoal = (g) => ({ id: g.id, text: g.text, supports: g.supports, progress: Number(g.progress), note: g.note });
const shapeActivity = (a) => ({ id: a.id, date: a.event_date, text: a.text,
  amount: a.amount_cents != null ? centsToDollars(a.amount_cents) : null, type: a.type });

function shapeDoc(raw) {
  const review = raw.review || {};
  return {
    participantId: raw.participantId,
    child: raw.child || {},
    planPeriod: raw.planPeriod || {},
    planDoc: review.planDoc || null,   // legacy stores this at doc top-level too
    budget: shapeBudget(raw.budget),
    providers: (raw.providers || []).map(shapeProvider),
    workers: (raw.workers || []).map(shapeWorker),
    timesheets: (raw.timesheets || []).map(shapeTimesheet),
    claims: (raw.claims || []).map(shapeClaim),
    invoices: (raw.invoices || []).map(shapeInvoice),
    goals: (raw.goals || []).map(shapeGoal),
    activity: (raw.activity || []).map(shapeActivity),
    review: { k7: !!review.k7, planDoc: review.planDoc || null, notes: review.notes || { worked: '', change: '' } },
  };
}
function planOf(cats) {
  const total = cats.reduce((s, c) => s + c.budget, 0);
  const spent = cats.reduce((s, c) => s + c.spent, 0);
  return { total, spent, remaining: total - spent, pct: total ? spent / total : 0 };
}
function syncPlanGlobals(doc) {
  const ps = new Date(doc.planPeriod.start || Date.now()), pe = new Date(doc.planPeriod.end || Date.now());
  const total = Math.max(1, daysBetween(ps, pe));
  const elapsed = Math.min(Math.max(0, daysBetween(ps, TODAY)), total);
  Object.assign(window, {
    PLAN_START: ps, PLAN_END: pe, PLAN_TOTAL_DAYS: total, PLAN_DAYS_ELAPSED: elapsed,
    PLAN_DAYS_LEFT: Math.max(0, daysBetween(TODAY, pe)),
    PLAN_TIME_PCT: Math.min(1, Math.max(0, elapsed / total)),
  });
}

const LS_LAST_EMAIL = 'haven_last_email'; // UX nicety only — never used for auth

const Store = {
  doc: null,
  // loading | need-auth | need-onboarding | synced | error
  status: 'loading',
  session: null,
  orgId: null,
  participantId: null,
  entitlements: null,   // null = not loaded yet; {} = loaded, fail-closed
  _listeners: new Set(),

  async init() {
    Supa.auth.onAuthStateChange((_event, session) => { this._onAuthChange(session); });
    const { data: { session } } = await Supa.auth.getSession();
    await this._onAuthChange(session);
    return this;
  },
  async _onAuthChange(session) {
    const hadUser = this.session && this.session.user && this.session.user.id;
    const nowUser = session && session.user && session.user.id;
    this.session = session;
    if (!session) { this.doc = null; this.orgId = null; this.participantId = null; this._setStatus('need-auth'); return; }
    if (hadUser === nowUser && this.doc) return;   // token refresh etc. — no need to reload everything
    await this._loadEverything();
  },

  async _loadEverything() {
    try {
      this._setStatus('loading');
      const { data: prof, error: profErr } = await Db.from('profiles').select('org_id').eq('id', this.session.user.id).maybeSingle();
      if (profErr) throw profErr;
      if (!prof || !prof.org_id) {
        this.lastError = 'No profile/org was found for your account (id ' + this.session.user.id + '). This usually means sign-up didn\'t finish setting it up.';
        this._setStatus('error'); return;
      }
      this.orgId = prof.org_id;
      const { data: par, error: parErr } = await Db.from('participants').select('id').eq('org_id', this.orgId).maybeSingle();
      if (parErr) throw parErr;
      if (!par) { this._setStatus('need-onboarding'); return; }
      this.participantId = par.id;
      await this._loadState();
      await this._loadEntitlements();
      this._setStatus('synced');
    } catch (e) {
      console.error('Haven: failed to load', e);
      this.lastError = (e && e.message) || String(e);
      this._setStatus('error');
    }
  },
  async _loadState() {
    const raw = await Rpc('get_state', { p_participant_id: this.participantId });
    this.doc = shapeDoc(raw);
    syncPlanGlobals(this.doc);
    this.notify();
  },
  async _refresh() { await this._loadState(); },

  async _loadEntitlements() {
    try {
      const r = await fetch(`${HAVEN_FUNCTIONS_BASE}/haven-check-features`, {
        headers: { Authorization: `Bearer ${this.session.access_token}` },
      });
      this.entitlements = r.ok ? await r.json() : {};
    } catch (e) { this.entitlements = {}; }   // fail closed
    this.notify();
  },
  hasFeature(key) {
    if (!this.entitlements) return false;
    const v = this.entitlements[key] ?? this.entitlements[`haven.${key}`];
    return v === true || (typeof v === 'string' && v !== '' && v !== 'false') || (typeof v === 'number' && v > 0);
  },

  subscribe(fn) { this._listeners.add(fn); return () => this._listeners.delete(fn); },
  notify() { this._listeners.forEach((fn) => fn()); },
  _setStatus(s) { this.status = s; this.notify(); },

  get plan() { return this.doc ? planOf(this.doc.budget) : { total: 0, spent: 0, remaining: 0, pct: 0 }; },

  get reminders() {
    if (!this.doc) return [];
    const out = [];
    const waiting = this.doc.invoices.filter((i) => !i.claimed);
    if (waiting.length) {
      const sum = waiting.reduce((s, i) => s + i.amount, 0);
      out.push({ id: 'r-claim', tone: 'amber', icon: 'receipt', title: `${waiting.length} invoice${waiting.length > 1 ? 's' : ''} ready to claim`,
        body: `${fmtMoney(sum, 2)} waiting to go to the NDIS. They'll usually land in your account within 2–3 days.`, cta: 'Review & claim', to: 'invoices' });
    }
    const pend = this.doc.timesheets.filter((t) => t.status === 'pending');
    if (pend.length) {
      const w = (id) => this.doc.workers.find((x) => x.id === id) || { rate: 0 };
      const hrs = pend.reduce((s, t) => s + t.hours, 0);
      const amt = pend.reduce((s, t) => s + t.hours * w(t.worker).rate, 0);
      out.push({ id: 'r-pay', tone: 'amber', icon: 'people', title: `${pend.length} timesheet${pend.length > 1 ? 's' : ''} need approving`,
        body: `${pend.length} timesheet${pend.length > 1 ? 's' : ''} · ${hrs} hours · about ${fmtMoney(amt)} before pay run.`, cta: 'Open pay run', to: 'workers' });
    }
    const soon = this.doc.providers
      .filter((p) => p.agreementEnds)
      .map((p) => ({ p, d: daysBetween(TODAY, new Date(p.agreementEnds)) }))
      .filter((x) => x.d <= 21).sort((a, b) => a.d - b.d)[0];
    if (soon) {
      out.push({ id: 'r-agree', tone: 'alert', icon: 'doc', title: `${soon.p.short} agreement ends in ${soon.d} days`,
        body: `Renew it before ${fmtDateShort(soon.p.agreementEnds)} so claims keep flowing.`, cta: 'View agreement', to: 'providers' });
    }
    out.push({ id: 'r-review', tone: 'primary', icon: 'spark', title: `Plan review in ${PLAN_DAYS_LEFT} days`,
      body: 'A little prep now saves a scramble later — your records are already gathered.', cta: 'Open review prep', to: 'review' });
    return out;
  },

  // ── auth ──
  async signIn(email, password) {
    const { error } = await Supa.auth.signInWithPassword({ email, password });
    if (error) throw new Error(error.message);
    try { localStorage.setItem(LS_LAST_EMAIL, email); } catch (e) {}
  },
  async signUp(email, password) {
    const r = await fetch(`${HAVEN_FUNCTIONS_BASE}/haven-auto-register`, {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    const j = await r.json().catch(() => ({}));
    if (!r.ok || !j.ok) {
      const msg = {
        not_subscriber: "We couldn't find an active Haven subscription for that email.",
        account_exists: 'An account already exists for that email — try signing in instead.',
      }[j.error] || j.error || 'Could not create your account.';
      throw new Error(msg);
    }
    await this.signIn(email, password);
  },
  async signOut() { await Supa.auth.signOut(); },

  async createParticipant({ name, ndis, parent, planStart, planEnd }) {
    await Rpc('create_participant', {
      p_name: name, p_ndis_number: ndis || null, p_parent_label: parent || 'Mum',
      p_plan_start: planStart || null, p_plan_end: planEnd || null,
    });
    await this._loadEverything();
  },

  // ── claims ──
  async addClaim({ provider, cat, sub, date, amount, invoice, invoiceId, file }) {
    const r = await Rpc('add_claim', {
      p_participant_id: this.participantId, p_provider: provider, p_cat: cat, p_sub: sub,
      p_date: date || null, p_amount_cents: dollarsToCents(amount),
      p_invoice: invoice || null, p_invoice_id: invoiceId || null, p_file: file || null,
    });
    await this._refresh();
    return r && r.id;
  },
  async setClaimStatus(id, status) { await Rpc('set_claim_status', { p_claim_id: id, p_status: status }); await this._refresh(); },
  async updateClaim(id, patch) {
    const p = { ...patch };
    if ('amount' in p) { p.amount_cents = dollarsToCents(p.amount); delete p.amount; }
    await Rpc('update_claim', { p_claim_id: id, p_patch: p });
    await this._refresh();
  },
  async deleteClaim(id) { await Rpc('delete_claim', { p_claim_id: id }); await this._refresh(); },

  // ── invoice vault ──
  async addInvoice(inv) {
    const body = { ...inv };
    if ('amount' in body) { body.amount_cents = dollarsToCents(body.amount); delete body.amount; }
    const r = await Rpc('add_invoice', { p_participant_id: this.participantId, p_invoice: body });
    await this._refresh();
    return r && r.id;
  },
  async deleteInvoice(id) { await Rpc('delete_invoice', { p_invoice_id: id }); await this._refresh(); },

  // ── support workers, timesheets, pay run ──
  async addTimesheet({ worker, date, hours, note }) {
    await Rpc('add_timesheet', { p_worker_id: worker, p_date: date, p_hours: parseFloat(hours) || 0, p_note: note || '' });
    await this._refresh();
  },
  async approveTimesheet(id) { await Rpc('approve_timesheet', { p_timesheet_id: id }); await this._refresh(); },
  async payApproved() {
    const r = await Rpc('pay_approved', { p_participant_id: this.participantId });
    await this._refresh();
    return centsToDollars(r && r.total_cents);
  },

  // ── plan review prep ──
  async setReviewNote(key, v) { await Rpc('set_review_note', { p_participant_id: this.participantId, p_key: key, p_value: v }); await this._refresh(); },
  async setReviewK7(v) { await Rpc('set_review_k7', { p_participant_id: this.participantId, p_value: !!v }); await this._refresh(); },
  async setPlanDoc(file) { await Rpc('set_plan_doc', { p_participant_id: this.participantId, p_file: file }); await this._refresh(); },

  // ── settings ──
  async setChild(patch) { await Rpc('set_child', { p_participant_id: this.participantId, p_patch: patch }); await this._refresh(); },
  async setPlanPeriod(patch) { await Rpc('set_plan_period', { p_participant_id: this.participantId, p_patch: patch }); await this._refresh(); },

  // budget sub CRUD — legacy callers pass (catKey, index); resolve to the real sub id here
  async subAdd(catKey) { await Rpc('sub_add', { p_participant_id: this.participantId, p_cat_key: catKey }); await this._refresh(); },
  async subPatch(catKey, i, patch) {
    const cat = this.doc.budget.find((c) => c.key === catKey);
    const sub = cat && cat.subs[i];
    if (!sub) return;
    const p = { ...patch };
    if ('budget' in p) { p.budget_cents = dollarsToCents(p.budget); delete p.budget; }
    if ('spent' in p) { p.spent_cents = dollarsToCents(p.spent); delete p.spent; }
    await Rpc('sub_patch', { p_sub_id: sub.id, p_patch: p });
    await this._refresh();
  },
  async subRemove(catKey, i) {
    const cat = this.doc.budget.find((c) => c.key === catKey);
    const sub = cat && cat.subs[i];
    if (!sub) return;
    await Rpc('sub_remove', { p_sub_id: sub.id });
    await this._refresh();
  },

  // generic list dispatch (providers/workers/goals) — routes to the specific RPC per table
  async listAdd(key, item) {
    const fn = { providers: 'add_provider', workers: 'add_worker', goals: 'add_goal' }[key];
    if (!fn) throw new Error(`listAdd: unknown list "${key}"`);
    const body = key === 'workers' && 'rate' in item ? { ...item, rate_cents: dollarsToCents(item.rate), rate: undefined } : item;
    const r = await Rpc(fn, { p_participant_id: this.participantId, p_item: body });
    await this._refresh();
    return r && r.id;
  },
  async listPatch(key, id, patch) {
    const fn = { providers: 'patch_provider', workers: 'patch_worker', goals: 'patch_goal' }[key];
    if (!fn) throw new Error(`listPatch: unknown list "${key}"`);
    const body = key === 'workers' && 'rate' in patch ? { ...patch, rate_cents: dollarsToCents(patch.rate), rate: undefined } : patch;
    await Rpc(fn, { p_id: id, p_patch: body });
    await this._refresh();
  },
  async listRemove(key, id) {
    const fn = { providers: 'remove_provider', workers: 'remove_worker', goals: 'remove_goal' }[key];
    if (!fn) throw new Error(`listRemove: unknown list "${key}"`);
    await Rpc(fn, { p_id: id });
    await this._refresh();
  },

  // ── export (read-only) ──
  downloadBackup() {
    if (!this.doc) return;
    const blob = new Blob([JSON.stringify(this.doc, null, 2)], { type: 'application/json' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'haven-backup-' + new Date().toISOString().slice(0, 10) + '.json';
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(a.href), 2000);
  },

  // ── not yet available in the hosted version — see README "Known gaps" ──
  async uploadFile() { throw new Error("File uploads aren't available yet — coming soon."); },
  restore() { throw new Error("Restoring a backup isn't available yet."); },
  startFresh() { throw new Error('Not available in the hosted version.'); },
  loadSample() { throw new Error('Not applicable to a live account.'); },
};

function useHaven() {
  const [, force] = React.useState(0);
  React.useEffect(() => Store.subscribe(() => force((n) => n + 1)), []);
  return Store;
}

Store.init();
Object.assign(window, { Store, useHaven });
