// Molara — shared data layer (talks to real API when available, localStorage otherwise)

const SPECIALTIES = [
  { k: 'emergency', n: 'Emergency dental consultation 24/7',      icon: 'shield',   count: 1 },
  { k: 'toothache', n: 'All types of tooth ache',                 icon: 'tooth',    count: 2 },
  { k: 'kids',      n: 'Kids tooth care',                         icon: 'baby',     count: 2 },
  { k: 'aligners',  n: 'Aligners and braces opinion',             icon: 'settings', count: 2 },
  { k: 'cosmetic',  n: 'Teeth whitening and cosmetic dental care', icon: 'sparkle',  count: 2 },
];

const DOCTORS = [
  { id: 1, n: 'Dr. Shivani Kumari',  creds: 'BDS', spec: 'General & Emergency Dentistry',        k: 'emergency', covers: ['emergency','toothache','kids','aligners','cosmetic'], y: 5, loc: 'Hyderabad',          r: 4.9,  rv: 15, fee: 499, langs: ['Hindi', 'English', 'Bangla', 'Maithili'], online: false, bio: 'Dr. Shivani Kumari handles general dental concerns and urgent triage across Molara, including tooth pain, swelling, broken teeth, bleeding after dental work, kids care, aligner or braces questions, and cosmetic guidance. She helps patients decide what can be managed online and when an in-clinic visit is needed.', avatar: 'SK', tone: 'rose',  wait: 'No waiting time' },
  { id: 2, n: 'Dr. Sakshi Sable',    creds: 'MDS', spec: 'Orthodontist',                         k: 'aligners',  covers: ['aligners'],                                  y: 5, loc: 'Pune',               r: 4.9,  rv: 12, fee: 699, langs: ['Hindi', 'English', 'Marathi'],          online: false, bio: 'Dr. Sakshi Sable focuses on braces, aligners, bite correction, retainer concerns, and second opinions for orthodontic treatment plans. She helps patients understand whether aligners or fixed braces may suit their dental condition.', avatar: 'SS', tone: 'amber', wait: '5 min' },
  { id: 3, n: 'Dr. Shirin Chavan',   creds: 'MDS', spec: 'Pediatric & Preventive Dentist',       k: 'kids',      covers: ['kids'],                                      y: 5, loc: 'Pune',               r: 4.8,  rv: 11, fee: 599, langs: ['English', 'Hindi', 'Marathi'],          online: false, bio: 'Dr. Shirin Chavan provides gentle online guidance for children\u2019s tooth pain, milk tooth concerns, early cavities, teething questions, brushing habits, and preventive care.', avatar: 'SC', tone: 'mint',  wait: '5 min' },
  { id: 4, n: 'Dr. Sanjana Murthy',  creds: 'BDS', spec: 'General Dentistry & Endodontics',      k: 'toothache', covers: ['toothache'],                                 y: 5, loc: 'Coimbatore, Tamil Nadu', r: 4.8, rv: 14, fee: 599, langs: ['Telugu', 'Tamil', 'Hindi', 'English'], online: false, bio: 'Dr. Sanjana Murthy supports patients with tooth ache, sensitivity, chewing pain, cavity concerns, and root canal second opinions.', avatar: 'SM', tone: 'blue',  wait: '5 min' },
  { id: 5, n: 'Dr. Neha Khandelwal', creds: 'BDS', spec: 'Cosmetic and Aesthetic Dentist',       k: 'cosmetic',  covers: ['cosmetic'],                                  y: 5, loc: 'Aligarh, UP',        r: 4.9,  rv: 10, fee: 699, langs: ['Hindi', 'English'],                    online: false, bio: 'Dr. Neha Khandelwal offers online opinions for teeth whitening, smile enhancement, stains, chipped tooth bonding, and cosmetic dentistry planning.', avatar: 'NK', tone: 'mint',  wait: '5 min' },
];

const DOC_BY_ID = (id) => DOCTORS.find(d => d.id === id) || DOCTORS[0];

const REVIEWS_KEY = 'molara.doctorReviews.v1';
const REVIEWS_EVENT = 'molara:doctor-reviews-change';
const MOLARA_API_ON = /^https?:$/.test(window.location.protocol);
const RECORDS_KEY = 'molara.records.v1';
const RECORDS_EVENT = 'molara:records-change';
const MESSAGES_KEY = 'molara.messages.v1';
const MESSAGES_EVENT = 'molara:messages-change';
const PRESENCE_EVENT = 'molara:presence-change';
const CONSULTS_KEY = 'molara.consults.v3';
const CONSULTS_EVENT = 'molara:consults-change';
const CONSULTS_ACCOUNT_KEY = 'molara.consults.account.v1';
const CONSULTS_DOCTOR_LOCAL_KEY = 'molara.consults.doctorLocal.v1';
const RX_READ_KEY = 'molara.prescriptions.read.v1';
const RX_READ_EVENT = 'molara:prescriptions-read-change';
const CONSULT_JOIN_EARLY_MS = 15 * 60 * 1000;
const CONSULT_DURATION_MS = 15 * 60 * 1000;
const CONSULT_MONTHS = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };

window.__molaraConsultsCache = window.__molaraConsultsCache || null;
window.__molaraRecordsCache = window.__molaraRecordsCache || null;
window.__molaraMessagesCache = window.__molaraMessagesCache || {};
window.__molaraPresence = window.__molaraPresence || new Set();

async function molaraFetch(path, options = {}) {
  if (!MOLARA_API_ON) return null;
  let sid = '';
  try { sid = localStorage.getItem('molara.sid.v1') || ''; } catch (_) {}
  const res = await fetch(path, {
    credentials: 'include',
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(sid ? { 'Authorization': `Bearer ${sid}` } : {}),
      ...(options.headers || {}),
    },
  });
  if (!res.ok) {
    const err = new Error(`Molara API ${res.status}`);
    err.status = res.status;
    throw err;
  }
  return res.json();
}

function doctorMatchesSpec(doc, specK) {
  if (!specK || specK === 'all') return true;
  return doc.k === specK || (Array.isArray(doc.covers) && doc.covers.includes(specK));
}

// ----- Doctor reviews (still purely local, fine for testing) -----
function readDoctorReviews() {
  try { const raw = localStorage.getItem(REVIEWS_KEY); const d = raw ? JSON.parse(raw) : {}; return d && typeof d === 'object' ? d : {}; }
  catch (_) { return {}; }
}
function writeDoctorReviews(data) {
  try { localStorage.setItem(REVIEWS_KEY, JSON.stringify(data || {})); } catch (_) {}
  window.dispatchEvent(new CustomEvent(REVIEWS_EVENT, { detail: data || {} }));
}
function doctorStats(doc) {
  const d = typeof doc === 'number' ? DOC_BY_ID(doc) : doc;
  const live = readDoctorReviews()[d.id];
  return { rating: Number(live?.r ?? d.r).toFixed(1), reviews: Number(live?.rv ?? d.rv) };
}
function submitDoctorReview(docId, rating) {
  const doc = DOC_BY_ID(docId);
  const c = doctorStats(doc);
  const n = c.reviews + 1;
  const r = ((Number(c.rating) * c.reviews) + Number(rating || 5)) / n;
  const all = readDoctorReviews();
  all[doc.id] = { r: Math.round(r * 10) / 10, rv: n };
  writeDoctorReviews(all);
  return all[doc.id];
}
function useDoctorReviews() {
  const [reviews, setReviews] = React.useState(() => readDoctorReviews());
  React.useEffect(() => {
    const sync = () => setReviews(readDoctorReviews());
    const eventSync = (e) => setReviews(e.detail || readDoctorReviews());
    window.addEventListener(REVIEWS_EVENT, eventSync);
    window.addEventListener('storage', sync);
    return () => { window.removeEventListener(REVIEWS_EVENT, eventSync); window.removeEventListener('storage', sync); };
  }, []);
  return reviews;
}

// ----- Presence (which doctors are signed in right now) -----
async function syncPresence() {
  if (!MOLARA_API_ON) return;
  try {
    const data = await molaraFetch('/api/presence');
    const set = new Set(Array.isArray(data?.online) ? data.online : []);
    window.__molaraPresence = set;
    window.dispatchEvent(new CustomEvent(PRESENCE_EVENT, { detail: set }));
  } catch (_) {}
}
function usePresence() {
  const [online, setOnline] = React.useState(() => window.__molaraPresence || new Set());
  React.useEffect(() => {
    const handler = (e) => setOnline(new Set(e.detail));
    window.addEventListener(PRESENCE_EVENT, handler);
    syncPresence();
    const t = MOLARA_API_ON ? setInterval(syncPresence, 8000) : null;
    return () => { window.removeEventListener(PRESENCE_EVENT, handler); if (t) clearInterval(t); };
  }, []);
  return online;
}
function isDoctorOnline(docId) {
  return (window.__molaraPresence || new Set()).has(docId);
}

// ----- Consults -----
function formatSlot(slot) {
  const s = String(slot || '2:15p');
  return /^\d{1,2}:\d{2}[ap]$/i.test(s) ? s.replace(/p$/i, ' PM').replace(/a$/i, ' AM') : s;
}
function consultTimeToMinutes(raw) {
  const m = /^(\d{1,2}):(\d{2})\s*(a|p|AM|PM)$/i.exec(String(raw || '').trim());
  if (!m) return null;
  let h = Number(m[1]);
  const min = Number(m[2]);
  const mer = m[3].toLowerCase()[0];
  if (mer === 'p' && h !== 12) h += 12;
  if (mer === 'a' && h === 12) h = 0;
  return h * 60 + min;
}
function consultIsEmergency(consult) {
  const time = String(consult?.time || consult?.slot || '').toLowerCase();
  const care = String(consult?.careType || consult?.reason || '').toLowerCase();
  return !!(consult?.emergency || time === 'immediate' || care.includes('emergency'));
}
function consultStartMs(consult) {
  if (!consult) return 0;
  if (consultIsEmergency(consult)) {
    return new Date(consult.callStartedAt || consult.startedAt || consult.paidAt || consult.createdAt || Date.now()).getTime();
  }
  const rawDate = String(consult.date || '');
  const rawTime = String(consult.slot || consult.time || '');
  const now = new Date();
  let base = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  if (/^Tomorrow\b/i.test(rawDate)) {
    base.setDate(base.getDate() + 1);
  } else if (!/^Today\b/i.test(rawDate)) {
    const m = /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2})\b/i.exec(rawDate);
    if (m) {
      base = new Date(now.getFullYear(), CONSULT_MONTHS[m[1].slice(0, 3)] ?? now.getMonth(), Number(m[2]));
      if (base.getTime() < now.getTime() - 180 * 24 * 60 * 60 * 1000) base.setFullYear(base.getFullYear() + 1);
    }
  }
  const minutes = consultTimeToMinutes(rawTime);
  if (minutes == null) return new Date(consult.createdAt || Date.now()).getTime();
  base.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
  return base.getTime();
}
function consultEffectiveStartIso(consult, role = '') {
  if (!consult) return new Date().toISOString();
  if (consultIsEmergency(consult)) {
    if (consult.callStartedAt || consult.startedAt) return consult.callStartedAt || consult.startedAt;
    return role === 'doctor' ? new Date().toISOString() : '';
  }
  const startMs = consultStartMs(consult);
  return new Date(startMs || Date.now()).toISOString();
}
function consultJoinStatusLabel(status) {
  if (!status || status.ok) return '';
  if (status.reason === 'too-early') return 'Opens 15 min before';
  if (status.reason === 'time-expired' || status.reason === 'time-window-closed') return 'Visit window closed';
  if (status.reason === 'ended') return 'Consultation ended';
  if (status.reason === 'unpaid') return 'Payment due';
  return 'Not available yet';
}
function todayBookingLabel() {
  const d = new Date();
  return `Today · ${d.toLocaleDateString('en-IN', { month: 'short' })} ${d.getDate()}`;
}
async function startEmergencyConsult(user) {
  const target = '/book?emergency=1';
  if (!user) {
    window.requireAuth ? window.requireAuth(target) : (window.navigate && window.navigate('/signin?next=' + encodeURIComponent(target)));
    return null;
  }
  if (user.role === 'doctor') {
    window.navigate && window.navigate('/doctor-dashboard');
    return null;
  }
  window.navigate && window.navigate(target);
  return null;
}
function readConsults() {
  if (Array.isArray(window.__molaraConsultsCache)) return window.__molaraConsultsCache;
  try { const raw = localStorage.getItem(CONSULTS_KEY); const list = raw ? JSON.parse(raw) : []; return Array.isArray(list) ? list : []; }
  catch (_) { return []; }
}
function accountKeyFromUser(user) {
  if (!user || user.role !== 'patient') return '';
  return String(user.userId || user.email || user.phone || '').trim().toLowerCase();
}
function currentAuthUser() {
  try {
    const raw = localStorage.getItem('molara.auth.v1');
    return raw ? JSON.parse(raw) : null;
  } catch (_) {
    return null;
  }
}
function currentAccountKey() {
  return accountKeyFromUser(currentAuthUser());
}
function readAccountConsultBackups() {
  try {
    const raw = localStorage.getItem(CONSULTS_ACCOUNT_KEY);
    const data = raw ? JSON.parse(raw) : {};
    return data && typeof data === 'object' ? data : {};
  } catch (_) {
    return {};
  }
}
function writeAccountConsultBackup(list, accountKey = currentAccountKey()) {
  if (!accountKey || !Array.isArray(list)) return;
  const all = readAccountConsultBackups();
  all[accountKey] = list;
  try { localStorage.setItem(CONSULTS_ACCOUNT_KEY, JSON.stringify(all)); } catch (_) {}
}
function readAccountConsultBackup(accountKey = currentAccountKey()) {
  const all = readAccountConsultBackups();
  return Array.isArray(all[accountKey]) ? all[accountKey] : [];
}
function readDoctorLocalConsultBackup() {
  try {
    const raw = localStorage.getItem(CONSULTS_DOCTOR_LOCAL_KEY);
    const list = raw ? JSON.parse(raw) : [];
    return Array.isArray(list) ? list : [];
  } catch (_) {
    return [];
  }
}
function rememberDoctorLocalConsults(list) {
  const current = readDoctorLocalConsultBackup();
  const merged = new Map(current.map((c) => [String(c.id), c]));
  (Array.isArray(list) ? list : []).forEach((c) => {
    if (consultLooksDoctorViewable(c)) merged.set(String(c.id), c);
  });
  const next = Array.from(merged.values()).slice(-80);
  try { localStorage.setItem(CONSULTS_DOCTOR_LOCAL_KEY, JSON.stringify(next)); } catch (_) {}
  return next;
}
function allLocalConsultCandidates() {
  const all = [];
  Object.values(readAccountConsultBackups()).forEach((list) => {
    if (Array.isArray(list)) all.push(...list);
  });
  all.push(...readDoctorLocalConsultBackup());
  all.push(...readConsults());
  const map = new Map();
  all.filter(consultLooksDoctorViewable).forEach((c) => map.set(String(c.id), c));
  return Array.from(map.values());
}
function consultMatchesDoctorUser(c, user) {
  if (!c || !user || user.role !== 'doctor') return false;
  const doc = DOC_BY_ID(user.doctorId);
  const same = (a, b) => String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase();
  return Number(c.docId) === Number(user.doctorId)
    || Number(c.doctorId) === Number(user.doctorId)
    || same(c.dentistName, user.name)
    || same(c.doctorName, user.name)
    || same(c.dentistName, doc?.n)
    || same(c.doctorName, doc?.n);
}
function mergeDoctorLocalConsults(serverList, user) {
  if (!user || user.role !== 'doctor') return serverList;
  const map = new Map((Array.isArray(serverList) ? serverList : []).map((c) => [String(c.id), c]));
  allLocalConsultCandidates()
    .filter((c) => consultMatchesDoctorUser(c, user))
    .forEach((c) => {
      if (!map.has(String(c.id))) map.set(String(c.id), c);
    });
  return Array.from(map.values())
    .sort((a, b) => new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0));
}
function writeConsults(list) {
  window.__molaraConsultsCache = Array.isArray(list) ? list : [];
  try { localStorage.setItem(CONSULTS_KEY, JSON.stringify(list)); } catch (_) {}
  writeAccountConsultBackup(window.__molaraConsultsCache);
  window.dispatchEvent(new CustomEvent(CONSULTS_EVENT, { detail: list }));
}
function consultLooksUploadable(c) {
  return !!(c && c.id && c.docId && c.reason && c.date && c.time);
}
function consultLooksDoctorViewable(c) {
  if (!c || !c.id || !(c.docId || c.dentistName || c.doctorName)) return false;
  return c.paymentStatus === 'paid'
    || c.status === 'completed'
    || c.completed
    || c.endedAt
    || c.completedAt
    || c.closedAt
    || c.prescription
    || c.prescribed;
}
async function restoreMissingLocalConsults(localList, serverList) {
  const authUser = currentAuthUser();
  if (!MOLARA_API_ON || authUser?.role !== 'patient') return serverList;
  const serverIds = new Set((Array.isArray(serverList) ? serverList : []).map((c) => String(c.id)));
  const missing = (Array.isArray(localList) ? localList : [])
    .filter((c) => consultLooksUploadable(c) && !serverIds.has(String(c.id)))
    .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0))
    .slice(-20);
  if (!missing.length) return serverList;
  for (const consult of missing) {
    try {
      await molaraFetch('/api/consults', { method: 'POST', body: JSON.stringify({ ...consult, restoredFromLocal: true }) });
    } catch (_) {}
  }
  try {
    const refreshed = await molaraFetch('/api/consults');
    return Array.isArray(refreshed) ? refreshed : serverList;
  } catch (_) {
    return serverList;
  }
}
function readPrescriptionReadIds() {
  try {
    const raw = localStorage.getItem(RX_READ_KEY);
    const ids = raw ? JSON.parse(raw) : [];
    return new Set(Array.isArray(ids) ? ids.map(String) : []);
  } catch (_) {
    return new Set();
  }
}
function writePrescriptionReadIds(ids) {
  const list = Array.from(ids || []).map(String);
  try { localStorage.setItem(RX_READ_KEY, JSON.stringify(list)); } catch (_) {}
  window.dispatchEvent(new CustomEvent(RX_READ_EVENT, { detail: list }));
}
function consultHasPrescriptionNotification(c) {
  return !!(c && c.id && (c.prescription || c.prescribed));
}
function unreadPrescriptionCount(consults = readConsults()) {
  const read = readPrescriptionReadIds();
  return (Array.isArray(consults) ? consults : [])
    .filter((c) => consultHasPrescriptionNotification(c) && !read.has(String(c.id)))
    .length;
}
function markPrescriptionsRead(ids) {
  const read = readPrescriptionReadIds();
  (Array.isArray(ids) ? ids : []).filter(Boolean).forEach((id) => read.add(String(id)));
  writePrescriptionReadIds(read);
  return read;
}
function useUnreadPrescriptionCount(consults) {
  const [tick, setTick] = React.useState(0);
  React.useEffect(() => {
    const sync = () => setTick((v) => v + 1);
    window.addEventListener(RX_READ_EVENT, sync);
    window.addEventListener(CONSULTS_EVENT, sync);
    return () => {
      window.removeEventListener(RX_READ_EVENT, sync);
      window.removeEventListener(CONSULTS_EVENT, sync);
    };
  }, []);
  return React.useMemo(() => unreadPrescriptionCount(consults || readConsults()), [consults, tick]);
}
async function syncConsultsFromServer() {
  if (!MOLARA_API_ON) return readConsults();
  const authUser = currentAuthUser();
  if (!authUser || !authUser.role) return readConsults();
  try {
    const list = await molaraFetch('/api/consults');
    writeConsults(Array.isArray(list) ? list : []);
    return readConsults();
  }
  catch (err) {
    if (err?.status === 401) {
      try {
        localStorage.removeItem('molara.sid.v1');
        localStorage.removeItem('molara.auth.v1');
      } catch (_) {}
      window.dispatchEvent(new CustomEvent('molara:auth-change', { detail: null }));
    }
    return readConsults();
  }
}
function clearMolaraCachedData({ keepAuth = true } = {}) {
  window.__molaraConsultsCache = [];
  window.__molaraRecordsCache = [];
  window.__molaraMessagesCache = {};
  const keys = [
    CONSULTS_KEY, CONSULTS_ACCOUNT_KEY, CONSULTS_DOCTOR_LOCAL_KEY,
    RECORDS_KEY, MESSAGES_KEY, RX_READ_KEY,
  ];
  if (!keepAuth) keys.push('molara.auth.v1', 'molara.sid.v1');
  keys.forEach((key) => {
    try { localStorage.removeItem(key); } catch (_) {}
  });
  try { localStorage.setItem(CONSULTS_KEY, JSON.stringify([])); } catch (_) {}
  try { localStorage.setItem(RECORDS_KEY, JSON.stringify([])); } catch (_) {}
  try { localStorage.setItem(MESSAGES_KEY, JSON.stringify({})); } catch (_) {}
  window.dispatchEvent(new CustomEvent(CONSULTS_EVENT, { detail: [] }));
  window.dispatchEvent(new CustomEvent(RECORDS_EVENT, { detail: [] }));
  window.dispatchEvent(new CustomEvent(RX_READ_EVENT, { detail: [] }));
}
async function resetMolaraTestData(token) {
  const resetToken = String(token || '').trim();
  if (!resetToken) throw new Error('Enter the reset token.');
  if (MOLARA_API_ON) {
    await molaraFetch('/api/admin/reset', {
      method: 'POST',
      headers: { 'x-molara-reset-token': resetToken },
      body: JSON.stringify({ token: resetToken }),
    });
  }
  clearMolaraCachedData({ keepAuth: false });
  return { ok: true };
}
window.addEventListener('molara:auth-change', (event) => {
  clearMolaraCachedData({ keepAuth: true });
  if (!event.detail?.role) {
    return;
  }
  syncConsultsFromServer();
  syncRecordsFromServer();
});
async function updateConsult(id, patch) {
  const current = readConsults();
  const next = current.map((c) => c.id === id ? { ...c, ...patch, updatedAt: new Date().toISOString() } : c);
  if (MOLARA_API_ON) {
    try {
      const saved = await molaraFetch(`/api/consults/${encodeURIComponent(id)}`, { method: 'PATCH', body: JSON.stringify(patch) });
      if (saved) {
        writeConsults(current.some((c) => c.id === id)
          ? current.map((c) => c.id === id ? saved : c)
          : [saved, ...current]);
        return saved;
      }
      return null;
    } catch (_) {
      writeConsults(current);
      return null;
    }
  }
  writeConsults(next);
  return next.find((c) => c.id === id) || null;
}
async function createConsult(payload) {
  const doc = DOC_BY_ID(payload.docId);
  const authUser = currentAuthUser() || {};
  const fee = payload.fee ?? doc.fee;
  const consult = {
    id: 'MLR-' + new Date().getFullYear() + '-' + String(Date.now()).slice(-5),
    createdAt: new Date().toISOString(),
    status: 'confirmed',
    docId: doc.id,
    doctorId: doc.id,
    dentistName: doc.n,
    doctorName: doc.n,
    dentistInitials: doc.avatar,
    dentistSpec: doc.spec,
    dentistCreds: doc.creds,
    dentistTone: doc.tone,
    patientId: authUser.userId || '',
    patientEmail: authUser.email || '',
    patientPhone: authUser.phone || '',
    patientName: payload.patientName || 'New patient',
    patientInitials: payload.patientInitials || 'PT',
    reason: payload.reason,
    severity: payload.severity,
    careType: payload.careType,
    note: payload.note || '',
    followUp: !!payload.followUp,
    followUpFor: payload.followUpFor || null,
    followUpFreeUntil: payload.followUpFreeUntil || null,
    date: payload.selectedDay || todayBookingLabel(),
    time: payload.time || formatSlot(payload.slot),
    mode: payload.mode || 'video',
    originalMode: payload.originalMode,
    fee,
    paymentStatus: payload.paymentStatus || 'pending',
    paidAmount: payload.paidAmount,
    paidMethod: payload.paidMethod,
    followUpCoveredByFree: !!payload.followUpCoveredByFree,
    followUpUpgradeAmount: payload.followUpUpgradeAmount,
    paidAt: payload.paidAt,
  };
  if (MOLARA_API_ON) {
    try {
      const saved = await molaraFetch('/api/consults', { method: 'POST', body: JSON.stringify(consult) });
      if (saved) {
        const list = await syncConsultsFromServer();
        return saved;
      }
    } catch (err) {
      if (err?.status === 401) {
        try {
          localStorage.removeItem('molara.sid.v1');
          localStorage.removeItem('molara.auth.v1');
        } catch (_) {}
        window.dispatchEvent(new CustomEvent('molara:auth-change', { detail: null }));
        if (window.alert) window.alert('Your login expired. Please sign in again, then book the appointment so the dentist can see it.');
        if (window.navigate) window.navigate('/signin?next=' + encodeURIComponent('/find'));
        return null;
      }
      if (err?.status === 409) {
        if (window.alert) window.alert('That consultation slot has just been booked by another patient. Please choose another time.');
        return null;
      }
      if (window.alert) window.alert('The appointment could not be saved to the server. Please try again in a moment.');
      return null;
    }
  }
  if (MOLARA_API_ON) return null;
  writeConsults([consult, ...readConsults().filter(c => c.id !== consult.id)]);
  return consult;
}

// ----- Payments / follow-up eligibility -----
const FREE_FOLLOWUP_LIMIT = 1;
const FOLLOWUP_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;

function consultRootId(consult, list) {
  let rootId = consult?.followUpFor || consult?.id || null;
  const seen = new Set();
  for (let i = 0; i < 8 && rootId && !seen.has(String(rootId)); i += 1) {
    seen.add(String(rootId));
    const next = (list || []).find((c) => String(c.id) === String(rootId));
    if (!next?.followUpFor) break;
    rootId = next.followUpFor;
  }
  return rootId;
}

function computeFollowupEligibility(list, docId, fromConsultId) {
  const all = Array.isArray(list) ? list : [];
  const findById = (id) => all.find((c) => String(c.id) === String(id));
  const paidTime = (c) => c?.paidAt ? new Date(c.paidAt).getTime() : 0;
  const normalizeAnchor = (consult) => {
    if (!consult) return null;
    const isFreeFollowup = consult.followUp && (consult.paidMethod === 'free-followup' || consult.paidMethod === 'followup-upgrade' || consult.followUpCoveredByFree || Number(consult.paidAmount || 0) === 0);
    if (consult.paymentStatus === 'paid' && consult.paidAt && !isFreeFollowup) return consult;
    const root = findById(consultRootId(consult, all));
    if (root?.paymentStatus === 'paid' && root.paidAt) return root;
    return null;
  };

  let anchor = fromConsultId ? normalizeAnchor(findById(fromConsultId)) : null;
  if (!anchor) {
    const paid = all
      .filter((c) => Number(c.docId) === Number(docId) && c.paymentStatus === 'paid' && c.paidAt)
      .sort((a, b) => paidTime(b) - paidTime(a));
    for (const consult of paid) {
      const candidate = normalizeAnchor(consult);
      if (candidate && Number(candidate.docId) === Number(docId)) {
        anchor = candidate;
        break;
      }
    }
  }

  if (!anchor) {
    return { eligible: false, freeLimit: FREE_FOLLOWUP_LIMIT, freeUsed: 0, freeRemaining: 0 };
  }

  const paidMs = paidTime(anchor);
  const expiresMs = paidMs + FOLLOWUP_WINDOW_MS;
  const rootId = anchor.id;
  const freeUsed = all.filter((c) => {
    if (!c.followUp || String(c.id) === String(anchor.id)) return false;
    if (String(consultRootId(c, all)) !== String(rootId)) return false;
    const cMs = new Date(c.paidAt || c.createdAt || 0).getTime();
    return c.paymentStatus === 'paid'
      && (c.paidMethod === 'free-followup' || c.paidMethod === 'followup-upgrade' || c.followUpCoveredByFree || Number(c.paidAmount || 0) === 0)
      && cMs >= paidMs
      && cMs <= expiresMs;
  }).length;

  const stillInsideWindow = Date.now() <= expiresMs;
  const freeRemaining = Math.max(0, FREE_FOLLOWUP_LIMIT - freeUsed);
  return {
    eligible: stillInsideWindow && freeUsed < FREE_FOLLOWUP_LIMIT,
    lastPaidConsultId: rootId,
    lastPaidAt: anchor.paidAt,
    expiresAt: new Date(expiresMs).toISOString(),
    freeLimit: FREE_FOLLOWUP_LIMIT,
    freeUsed,
    freeRemaining,
  };
}

// Returns { eligible, lastPaidConsultId, lastPaidAt, expiresAt, freeUsed, freeRemaining }.
// Free follow-ups are limited to 1 within 7 days for the same dentist/treatment.
async function checkFollowupEligible(docId, fromConsultId) {
  if (!MOLARA_API_ON) {
    return computeFollowupEligibility(readConsults(), docId, fromConsultId);
  }
  const from = fromConsultId ? `&from=${encodeURIComponent(fromConsultId)}` : '';
  try { return await molaraFetch(`/api/payments/eligible?docId=${encodeURIComponent(docId)}${from}`); }
  catch (_) { return { eligible: false, freeLimit: FREE_FOLLOWUP_LIMIT, freeUsed: 0, freeRemaining: 0 }; }
}

async function chargeConsult({ consultId, amount, last4, method = 'card' }) {
  const markLocalPaid = () => {
    const now = new Date().toISOString();
    let saved = null;
    const list = readConsults().map((c) => {
      if (c.id !== consultId) return c;
      const paidMethod = c.followUpCoveredByFree ? 'followup-upgrade' : method;
      saved = { ...c, paymentStatus: 'paid', paidAt: now, paidAmount: amount, paidMethod, paymentChannel: method, paidLast4: last4, updatedAt: now };
      return saved;
    });
    writeConsults(list);
    return saved;
  };
  if (!MOLARA_API_ON) return markLocalPaid();
  try {
    const saved = await molaraFetch('/api/payments/charge', {
      method: 'POST',
      body: JSON.stringify({ consultId, amount, last4, method }),
    });
    if (saved) {
      const current = readConsults();
      writeConsults(current.some((c) => c.id === saved.id)
        ? current.map((c) => c.id === saved.id ? saved : c)
        : [saved, ...current]);
    }
    return saved;
  } catch (_) {
    return markLocalPaid();
  }
}

function consultBlocksScheduledSlot(c) {
  if (!c || consultIsEmergency(c)) return false;
  if (c.status === 'cancelled' || c.status === 'canceled') return false;
  if (c.status === 'completed' || c.status === 'awaiting-prescription' || c.prescription || c.prescribed || c.closedAt || c.completedAt || c.endedAt) return false;
  return !!(c.docId && c.date && (c.time || c.slot));
}

function consultSlotIsBooked(list, docId, dateLabel, timeLabel, excludeId = '') {
  const expectedDoc = Number(docId);
  const expectedDate = String(dateLabel || '').trim().toLowerCase();
  const expectedTime = String(timeLabel || '').trim().toLowerCase();
  return (Array.isArray(list) ? list : []).some((c) => {
    if (!consultBlocksScheduledSlot(c)) return false;
    if (excludeId && String(c.id) === String(excludeId)) return false;
    if (Number(c.docId || c.doctorId) !== expectedDoc) return false;
    const cDate = String(c.date || '').trim().toLowerCase();
    const cTime = String(c.time || formatSlot(c.slot)).trim().toLowerCase();
    return cDate === expectedDate && cTime === expectedTime;
  });
}

async function getBookedSlotsForDoctor(docId, dates = []) {
  const dayList = Array.isArray(dates) ? dates.filter(Boolean) : [];
  const booked = {};
  const addBooked = (date, time) => {
    if (!date || !time) return;
    const key = String(date);
    booked[key] = booked[key] || [];
    if (!booked[key].some((t) => String(t).toLowerCase() === String(time).toLowerCase())) booked[key].push(time);
  };
  readConsults()
    .filter((c) => consultBlocksScheduledSlot(c) && Number(c.docId || c.doctorId) === Number(docId))
    .forEach((c) => addBooked(c.date, c.time || formatSlot(c.slot)));
  if (MOLARA_API_ON && docId && dayList.length) {
    try {
      const qs = new URLSearchParams({ docId: String(docId), dates: dayList.join('|') });
      const server = await molaraFetch(`/api/availability?${qs.toString()}`);
      Object.entries(server?.booked || {}).forEach(([date, times]) => {
        (Array.isArray(times) ? times : []).forEach((time) => addBooked(date, time));
      });
    } catch (_) {}
  }
  return booked;
}

// Is this consult joinable right now?
// Emergency visits open immediately after payment. Normal visits open 15 min
// before the scheduled time and close 15 min after the scheduled start.
function consultIsJoinable(consult) {
  if (!consult) return { ok: false, reason: 'no-consult' };
  if (consult.status === 'completed' || consult.status === 'awaiting-prescription' || consult.prescription || consult.prescribed || consult.closedAt || consult.completedAt || consult.endedAt) {
    return { ok: false, reason: 'ended' };
  }
  if (consult.paymentStatus !== 'paid') return { ok: false, reason: 'unpaid' };
  const now = Date.now();
  const emergency = consultIsEmergency(consult);
  const startMs = consultStartMs(consult);
  const startedAt = consult.callStartedAt || consult.startedAt;
  if (emergency) {
    if (startedAt && now - new Date(startedAt).getTime() >= CONSULT_DURATION_MS) {
      return { ok: false, reason: 'time-expired', emergency, startMs: new Date(startedAt).getTime() };
    }
    return {
      ok: true,
      emergency,
      startMs,
      viaFollowup: !!(consult.followUp && (consult.paidMethod === 'free-followup' || consult.paidMethod === 'followup-upgrade' || consult.followUpCoveredByFree || Number(consult.paidAmount || 0) === 0)),
    };
  }
  const opensAt = startMs - CONSULT_JOIN_EARLY_MS;
  const closesAt = startMs + CONSULT_DURATION_MS;
  if (now < opensAt) {
    return { ok: false, reason: 'too-early', emergency, startMs, opensAt, closesAt };
  }
  if (now >= closesAt) {
    return { ok: false, reason: 'time-window-closed', emergency, startMs, opensAt, closesAt };
  }
  return {
    ok: true,
    emergency,
    startMs,
    opensAt,
    closesAt,
    viaFollowup: !!(consult.followUp && (consult.paidMethod === 'free-followup' || consult.paidMethod === 'followup-upgrade' || consult.followUpCoveredByFree || Number(consult.paidAmount || 0) === 0)),
  };
}
function latestConsult() { return readConsults()[0] || null; }
function useConsults() {
  const [consults, setConsults] = React.useState(() => readConsults());
  React.useEffect(() => {
    const sync = () => setConsults(readConsults());
    const eventSync = (e) => setConsults(Array.isArray(e.detail) ? e.detail : readConsults());
    window.addEventListener(CONSULTS_EVENT, eventSync);
    window.addEventListener('storage', sync);
    syncConsultsFromServer().then(sync);
    const poll = MOLARA_API_ON ? setInterval(() => syncConsultsFromServer().then(sync), 3000) : null;
    return () => { window.removeEventListener(CONSULTS_EVENT, eventSync); window.removeEventListener('storage', sync); if (poll) clearInterval(poll); };
  }, []);
  return consults;
}
function clearConsults() { writeConsults([]); }

// ----- Records -----
function readRecords() {
  if (Array.isArray(window.__molaraRecordsCache)) return window.__molaraRecordsCache;
  try { const raw = localStorage.getItem(RECORDS_KEY); const list = raw ? JSON.parse(raw) : []; return Array.isArray(list) ? list : []; }
  catch (_) { return []; }
}
function writeRecords(list) {
  window.__molaraRecordsCache = Array.isArray(list) ? list : [];
  try { localStorage.setItem(RECORDS_KEY, JSON.stringify(window.__molaraRecordsCache)); } catch (_) {}
  window.dispatchEvent(new CustomEvent(RECORDS_EVENT, { detail: window.__molaraRecordsCache }));
}
async function syncRecordsFromServer() {
  if (!MOLARA_API_ON) return readRecords();
  try { const list = await molaraFetch('/api/records'); writeRecords(Array.isArray(list) ? list : []); }
  catch (_) {}
  return readRecords();
}
async function createRecord(record) {
  if (MOLARA_API_ON) {
    try {
      const saved = await molaraFetch('/api/records', { method: 'POST', body: JSON.stringify(record) });
      if (saved) {
        await syncRecordsFromServer();
        return saved;
      }
    } catch (_) {}
  }
  const next = { id: record.id || String(Date.now()), createdAt: record.createdAt || new Date().toISOString(), ...record };
  writeRecords([next, ...readRecords()]);
  return next;
}
function useRecords() {
  const [records, setRecords] = React.useState(() => readRecords());
  React.useEffect(() => {
    const sync = () => setRecords(readRecords());
    const eventSync = (e) => setRecords(Array.isArray(e.detail) ? e.detail : readRecords());
    window.addEventListener(RECORDS_EVENT, eventSync);
    window.addEventListener('storage', sync);
    syncRecordsFromServer().then(sync);
    const poll = MOLARA_API_ON ? setInterval(() => syncRecordsFromServer().then(sync), 4000) : null;
    return () => { window.removeEventListener(RECORDS_EVENT, eventSync); window.removeEventListener('storage', sync); if (poll) clearInterval(poll); };
  }, []);
  return records;
}

// ----- Messages -----
function readMessages(consultId = 'default') {
  if (Array.isArray(window.__molaraMessagesCache[consultId])) return window.__molaraMessagesCache[consultId];
  try { const raw = localStorage.getItem(MESSAGES_KEY); const all = raw ? JSON.parse(raw) : {}; return Array.isArray(all[consultId]) ? all[consultId] : []; }
  catch (_) { return []; }
}
function writeMessages(consultId, list) {
  window.__molaraMessagesCache[consultId] = Array.isArray(list) ? list : [];
  try {
    const raw = localStorage.getItem(MESSAGES_KEY);
    const all = raw ? JSON.parse(raw) : {};
    all[consultId] = window.__molaraMessagesCache[consultId];
    localStorage.setItem(MESSAGES_KEY, JSON.stringify(all));
  } catch (_) {}
  window.dispatchEvent(new CustomEvent(MESSAGES_EVENT, { detail: { consultId, list: window.__molaraMessagesCache[consultId] } }));
}
async function syncMessagesFromServer(consultId = 'default') {
  if (!MOLARA_API_ON) return readMessages(consultId);
  try { const list = await molaraFetch(`/api/messages?consultId=${encodeURIComponent(consultId)}`); writeMessages(consultId, Array.isArray(list) ? list : []); }
  catch (_) {}
  return readMessages(consultId);
}
async function sendVisitMessage(consultId, from, text) {
  const trimmed = String(text || '').trim();
  if (!trimmed) return null;
  if (MOLARA_API_ON) {
    try {
      const saved = await molaraFetch('/api/messages', { method: 'POST', body: JSON.stringify({ consultId, from, text: trimmed }) });
      if (saved) {
        await syncMessagesFromServer(consultId);
        return saved;
      }
    } catch (_) {}
  }
  const next = { id: String(Date.now()), consultId, from, text: trimmed, createdAt: new Date().toISOString() };
  writeMessages(consultId, [...readMessages(consultId), next]);
  return next;
}
function useVisitMessages(consultId = 'default') {
  const [messages, setMessages] = React.useState(() => readMessages(consultId));
  React.useEffect(() => {
    const sync = () => setMessages(readMessages(consultId));
    const eventSync = (e) => { if (e.detail?.consultId === consultId) setMessages(e.detail.list || readMessages(consultId)); };
    window.addEventListener(MESSAGES_EVENT, eventSync);
    syncMessagesFromServer(consultId).then(sync);
    const poll = MOLARA_API_ON ? setInterval(() => syncMessagesFromServer(consultId).then(sync), 1500) : null;
    return () => { window.removeEventListener(MESSAGES_EVENT, eventSync); if (poll) clearInterval(poll); };
  }, [consultId]);
  return messages;
}

const TONE_BG = { rose: 'oklch(94% 0.04 25)', mint: 'oklch(94% 0.05 165)', blue: 'oklch(94% 0.04 240)', amber: 'oklch(94% 0.05 80)' };
const TONE_FG = { rose: 'oklch(45% 0.12 25)', mint: 'oklch(38% 0.10 165)', blue: 'oklch(38% 0.13 240)', amber: 'oklch(40% 0.10 80)' };

Object.assign(window, {
  DOCTORS, SPECIALTIES, DOC_BY_ID, TONE_BG, TONE_FG,
  CONSULTS_KEY, CONSULTS_EVENT, REVIEWS_KEY, REVIEWS_EVENT, RX_READ_KEY, RX_READ_EVENT,
  RECORDS_KEY, RECORDS_EVENT, MESSAGES_KEY, MESSAGES_EVENT, PRESENCE_EVENT,
  MOLARA_API_ON, molaraFetch,
  doctorMatchesSpec, readDoctorReviews, writeDoctorReviews,
  doctorStats, submitDoctorReview, useDoctorReviews,
  readConsults, writeConsults, syncConsultsFromServer,
  updateConsult, createConsult, startEmergencyConsult, latestConsult, useConsults, clearConsults,
  clearMolaraCachedData, resetMolaraTestData,
  readPrescriptionReadIds, markPrescriptionsRead, unreadPrescriptionCount, useUnreadPrescriptionCount,
  readRecords, writeRecords, createRecord, useRecords, syncRecordsFromServer,
  readMessages, writeMessages, syncMessagesFromServer, sendVisitMessage, useVisitMessages,
  formatSlot, syncPresence, usePresence, isDoctorOnline,
  checkFollowupEligible, chargeConsult, consultIsJoinable,
  consultBlocksScheduledSlot, consultSlotIsBooked, getBookedSlotsForDoctor,
  consultIsEmergency, consultStartMs, consultEffectiveStartIso, consultJoinStatusLabel,
});
